Files
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

430 lines
15 KiB
Plaintext

---
title: UI primitives
created_at: 12/25/2025
updated_at: 12/25/2025
keywords:
- ui
- primitives
- button
- icon
- menu
- dialog
- components
status: published
date: 12/25/2025
readability: 9
voice: 8
completeness: 8
accuracy: 10
notes: ''
---
The `tldraw` package exports a set of UI primitive components that you can use when building custom interfaces. These components match the look and feel of tldraw's default UI and integrate with the editor's theming, translations, and accessibility features.
Use these primitives when you want your custom UI to feel like a natural part of tldraw rather than something bolted on. See the [UI primitives example](/examples/ui/ui-primitives) for all of them in one place.
## Buttons
The button system consists of [TldrawUiButton](?) and its companion components for icons, labels, and check indicators.
### TldrawUiButton
The base button component with several visual variants:
```tsx
import { TldrawUiButton, TldrawUiButtonIcon, TldrawUiButtonLabel } from 'tldraw'
function MyButtons() {
return (
<>
<TldrawUiButton type="normal" onClick={() => console.log('clicked')}>
<TldrawUiButtonLabel>Normal</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton type="primary" onClick={() => console.log('clicked')}>
<TldrawUiButtonLabel>Primary</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton type="danger" onClick={() => console.log('clicked')}>
<TldrawUiButtonLabel>Danger</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton type="icon" onClick={() => console.log('clicked')}>
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButton>
</>
)
}
```
The `type` prop controls the button's appearance:
| Type | Description |
| --------- | ---------------------------------------- |
| `normal` | Standard button for general actions |
| `primary` | Emphasized button for primary actions |
| `danger` | Red button for destructive actions |
| `low` | Subtle button with minimal visual weight |
| `icon` | Square button sized for a single icon |
| `tool` | Tool button style used in the toolbar |
| `menu` | Button style used inside menus |
| `help` | Style used for help/info buttons |
Use `isActive` to indicate a selected or active state, and `tooltip` to show a tooltip on hover:
```tsx
<TldrawUiButton type="tool" isActive={true} tooltip="Draw">
<TldrawUiButtonIcon icon="tool-pencil" />
</TldrawUiButton>
```
### Button sub-components
Build up button contents using these components:
```tsx
import { TldrawUiButton, TldrawUiButtonIcon, TldrawUiButtonLabel, TldrawUiButtonCheck } from 'tldraw'
// Icon button
<TldrawUiButton type="icon">
<TldrawUiButtonIcon icon="trash" />
</TldrawUiButton>
// Button with icon and label
<TldrawUiButton type="menu">
<TldrawUiButtonIcon icon="plus" />
<TldrawUiButtonLabel>Add item</TldrawUiButtonLabel>
</TldrawUiButton>
// Button with checkmark (for toggles in menus)
<TldrawUiButton type="menu">
<TldrawUiButtonCheck checked={true} />
<TldrawUiButtonLabel>Show grid</TldrawUiButtonLabel>
</TldrawUiButton>
```
## Icons
[TldrawUiIcon](?) renders icons from tldraw's icon set. Icons are SVG masks filled with the current text color.
```tsx
import { TldrawUiIcon } from 'tldraw'
<TldrawUiIcon icon="tool-pencil" label="Draw tool" />
<TldrawUiIcon icon="arrow-left" label="Go back" small />
<TldrawUiIcon icon="check" label="Complete" color="green" />
```
The `label` prop is required for accessibility—it becomes the icon's `aria-label`. Use `small` for a smaller icon size.
You can also pass a custom React element instead of an icon name:
```tsx
<TldrawUiIcon icon={<div className="my-custom-icon">★</div>} label="Favorite" />
```
## Menu primitives
When adding items to tldraw's menus, use these components to match the default menu styling and behavior. Menu primitives read a menu context to decide how to render, so they must be placed inside one of tldraw's menus (for example a custom `MainMenu` or `ContextMenu` component) or wrapped in a [TldrawUiMenuContextProvider](?). See the [custom menus example](/examples/ui/custom-menus).
### TldrawUiMenuItem
[TldrawUiMenuItem](?) is the main component for menu items. It adapts its rendering to the menu it's in (dropdown, context menu, toolbar, and so on):
```tsx
import { TldrawUiMenuGroup, TldrawUiMenuItem } from 'tldraw'
function MyMenuGroup() {
return (
<TldrawUiMenuGroup id="my-actions">
<TldrawUiMenuItem
id="my-action"
label="Do something"
iconLeft="plus"
kbd="cmd+shift+d"
onSelect={() => {
console.log('action triggered')
}}
/>
</TldrawUiMenuGroup>
)
}
```
The props are:
| Prop | Description |
| ------------ | ------------------------------------------------------------------------------- |
| `id` | Unique identifier for the menu item |
| `label` | Display text (supports translation keys) |
| `icon` | Icon for icon-style menus and toolbars (not shown in dropdown or context menus) |
| `iconLeft` | Icon shown on the left in dropdown and context menus |
| `kbd` | Keyboard shortcut to display |
| `onSelect` | Called when the item is clicked |
| `disabled` | Whether the item is disabled |
| `readonlyOk` | If true, item is shown even in readonly mode |
| `isSelected` | Whether the item shows as selected (for toolbar items) |
| `spinner` | Show a loading spinner |
| `noClose` | Prevent the menu from closing when clicked |
### TldrawUiMenuGroup
[TldrawUiMenuGroup](?) groups related menu items together. In dropdown menus, groups are separated by dividers:
```tsx
<TldrawUiMenuGroup id="clipboard">
<TldrawUiMenuItem id="cut" label="Cut" kbd="cmd+x" onSelect={handleCut} />
<TldrawUiMenuItem id="copy" label="Copy" kbd="cmd+c" onSelect={handleCopy} />
<TldrawUiMenuItem id="paste" label="Paste" kbd="cmd+v" onSelect={handlePaste} />
</TldrawUiMenuGroup>
```
### TldrawUiMenuSubmenu
[TldrawUiMenuSubmenu](?) creates a nested submenu:
```tsx
<TldrawUiMenuSubmenu id="export" label="Export as...">
<TldrawUiMenuGroup id="formats">
<TldrawUiMenuItem id="png" label="PNG" onSelect={exportPng} />
<TldrawUiMenuItem id="svg" label="SVG" onSelect={exportSvg} />
<TldrawUiMenuItem id="json" label="JSON" onSelect={exportJson} />
</TldrawUiMenuGroup>
</TldrawUiMenuSubmenu>
```
### TldrawUiMenuCheckboxItem
[TldrawUiMenuCheckboxItem](?) is a menu item with a checkbox:
```tsx
<TldrawUiMenuCheckboxItem
id="snap-to-grid"
label="Snap to grid"
checked={snapEnabled}
onSelect={() => {
setSnapEnabled(!snapEnabled)
}}
/>
```
The `onSelect` callback receives a `source` parameter, a [TLUiEventSource](?) such as `'main-menu'` or `'context-menu'`, taken from the surrounding menu context. Ignore it if you don't need to tell sources apart.
To put an existing action or tool in a menu, use [TldrawUiMenuActionItem](?) or [TldrawUiMenuToolItem](?) with the action or tool id; they fill in the label, icon, and shortcut for you.
## Dialogs
Build modal dialogs using tldraw's dialog primitives ([TldrawUiDialogHeader](?), [TldrawUiDialogTitle](?), [TldrawUiDialogCloseButton](?), [TldrawUiDialogBody](?), and [TldrawUiDialogFooter](?)) and open them with [useDialogs](?). These components handle accessibility, focus management, and styling.
```tsx
import {
TldrawUiDialogHeader,
TldrawUiDialogTitle,
TldrawUiDialogCloseButton,
TldrawUiDialogBody,
TldrawUiDialogFooter,
TldrawUiButton,
TldrawUiButtonLabel,
useDialogs,
} from 'tldraw'
function MyDialog({ onClose }: { onClose(): void }) {
return (
<>
<TldrawUiDialogHeader>
<TldrawUiDialogTitle>Confirm deletion</TldrawUiDialogTitle>
<TldrawUiDialogCloseButton />
</TldrawUiDialogHeader>
<TldrawUiDialogBody style={{ maxWidth: 350 }}>
Are you sure you want to delete this item? This action cannot be undone.
</TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton type="normal" onClick={onClose}>
<TldrawUiButtonLabel>Cancel</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton
type="danger"
onClick={() => {
deleteItem()
onClose()
}}
>
<TldrawUiButtonLabel>Delete</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDialogFooter>
</>
)
}
// Show the dialog using the useDialogs hook
function MyComponent() {
const { addDialog } = useDialogs()
return <button onClick={() => addDialog({ component: MyDialog })}>Delete item</button>
}
```
The `onClose` function is passed to your dialog component automatically. Call it to dismiss the dialog.
## Input
[TldrawUiInput](?) is a styled text input with built-in handling for Enter (confirm) and Escape (cancel):
```tsx
<TldrawUiInput
label="Name"
defaultValue="Untitled"
onComplete={(value) => {
// Called when user presses Enter
saveName(value)
}}
onCancel={(value) => {
// Called when user presses Escape
// Value is reset to initial value
}}
onValueChange={(value) => {
// Called on every keystroke
}}
autoSelect // Select all text on focus
autoFocus
/>
```
Add icons to the input using `iconLeft` (left side) or `icon` (right side):
```tsx
<TldrawUiInput iconLeft="zoom-in" placeholder="Search shapes..." onValueChange={setSearchQuery} />
<TldrawUiInput icon="check" placeholder="Confirmed value" />
```
## Layout
[TldrawUiRow](?), [TldrawUiColumn](?), and [TldrawUiGrid](?) organize UI controls with proper spacing and orientation-aware tooltips.
```tsx
import { TldrawUiRow, TldrawUiColumn, TldrawUiGrid } from 'tldraw'
// Horizontal row of buttons
<TldrawUiRow>
<TldrawUiButton type="icon"><TldrawUiButtonIcon icon="align-left" /></TldrawUiButton>
<TldrawUiButton type="icon"><TldrawUiButtonIcon icon="align-center-horizontal" /></TldrawUiButton>
<TldrawUiButton type="icon"><TldrawUiButtonIcon icon="align-right" /></TldrawUiButton>
</TldrawUiRow>
// Vertical column
<TldrawUiColumn>
<TldrawUiButton type="menu"><TldrawUiButtonLabel>Option 1</TldrawUiButtonLabel></TldrawUiButton>
<TldrawUiButton type="menu"><TldrawUiButtonLabel>Option 2</TldrawUiButtonLabel></TldrawUiButton>
</TldrawUiColumn>
// 4-column grid (useful for color pickers, shape selectors, etc.)
<TldrawUiGrid>
{colors.map(color => (
<TldrawUiButton key={color} type="icon" onClick={() => setColor(color)}>
<div style={{ background: color, width: 16, height: 16 }} />
</TldrawUiButton>
))}
</TldrawUiGrid>
```
These components set up an orientation context. By default, tooltips appear below items in rows and to the right of items in columns; nested layouts inherit the side until the orientation changes. Pass `tooltipSide` to override it.
## Other primitives
### TldrawUiKbd
[TldrawUiKbd](?) displays a keyboard shortcut using platform-specific symbols. It is hidden on small mobile breakpoints unless you pass `visibleOnMobileLayout`:
```tsx
<TldrawUiKbd>cmd+shift+d</TldrawUiKbd>
```
### TldrawUiSlider
[TldrawUiSlider](?) is a slider control with discrete integer steps rather than a continuous range:
```tsx
<TldrawUiSlider
title="Opacity"
label="style-panel.opacity"
value={5}
steps={10}
onValueChange={(value) => console.log(value)}
/>
```
The props are:
| Prop | Description |
| --------------- | -------------------------------------------------------------------- |
| `title` | Title text, combined with the label for the tooltip and `aria-label` |
| `label` | Translation key for the label |
| `value` | Current value (`min` to `steps`), or `null` |
| `steps` | Maximum value |
| `min` | Optional minimum value (defaults to 0) |
| `onValueChange` | Called with the new value when it changes |
| `onHistoryMark` | Called on pointer down so you can mark a history stopping point |
### TldrawUiPopover
[TldrawUiPopover](?) shows content next to a trigger element:
```tsx
<TldrawUiPopover id="my-popover">
<TldrawUiPopoverTrigger>
<TldrawUiButton type="icon">
<TldrawUiButtonIcon icon="dots-vertical" />
</TldrawUiButton>
</TldrawUiPopoverTrigger>
<TldrawUiPopoverContent side="bottom">
<div style={{ padding: 8 }}>Popover content here</div>
</TldrawUiPopoverContent>
</TldrawUiPopover>
```
The `id` prop is required and used to track the popover's open state. The `side` prop on `TldrawUiPopoverContent` controls which side of the trigger the popover appears on.
### TldrawUiDropdownMenu
[TldrawUiDropdownMenuRoot](?) and its companions build a dropdown menu on Radix UI:
```tsx
<TldrawUiDropdownMenuRoot id="my-dropdown">
<TldrawUiDropdownMenuTrigger>
<TldrawUiButton type="normal">
<TldrawUiButtonLabel>Options</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent>
<TldrawUiDropdownMenuGroup>
<TldrawUiDropdownMenuItem>
<TldrawUiButton type="menu" onClick={handleEdit}>
<TldrawUiButtonLabel>Edit</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
<TldrawUiDropdownMenuItem>
<TldrawUiButton type="menu" onClick={handleDuplicate}>
<TldrawUiButtonLabel>Duplicate</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
<TldrawUiDropdownMenuItem>
<TldrawUiButton type="menu" onClick={handleDelete}>
<TldrawUiButtonLabel>Delete</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
</TldrawUiDropdownMenuGroup>
</TldrawUiDropdownMenuContent>
</TldrawUiDropdownMenuRoot>
```
The `id` prop is required on `TldrawUiDropdownMenuRoot`. Each `TldrawUiDropdownMenuItem` wraps a child element (typically a button) and handles the dropdown behavior.
Other exported primitives include [TldrawUiToolbar](?) and [TldrawUiToolbarButton](?), [TldrawUiTooltip](?), [TldrawUiSelect](?), [TldrawUiContextualToolbar](?), and the dropdown submenu and checkbox item components.
## Related examples
- [UI primitives](/examples/ui/ui-primitives) - A showcase of the UI primitive components
- [Custom menus](/examples/ui/custom-menus) - Add items to tldraw's menus using menu primitives
- [Toasts and dialogs](/examples/ui/toasts-and-dialogs) - Show toasts and custom dialogs
- [Custom UI](/examples/ui/custom-ui) - Build a completely custom interface