apps/docs/content/sdk-features/ui-primitives.mdx
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 for all of them in one place.
The button system consists of TldrawUiButton and its companion components for icons, labels, and check indicators.
The base button component with several visual variants:
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:
<TldrawUiButton type="tool" isActive={true} tooltip="Draw">
<TldrawUiButtonIcon icon="tool-pencil" />
</TldrawUiButton>
Build up button contents using these components:
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>
TldrawUiIcon renders icons from tldraw's icon set. Icons are SVG masks filled with the current text color.
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:
<TldrawUiIcon icon={<div className="my-custom-icon">★</div>} label="Favorite" />
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.
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):
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 groups related menu items together. In dropdown menus, groups are separated by dividers:
<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 creates a nested submenu:
<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 is a menu item with a checkbox:
<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.
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.
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.
TldrawUiInput is a styled text input with built-in handling for Enter (confirm) and Escape (cancel):
<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):
<TldrawUiInput iconLeft="zoom-in" placeholder="Search shapes..." onValueChange={setSearchQuery} />
<TldrawUiInput icon="check" placeholder="Confirmed value" />
TldrawUiRow, TldrawUiColumn, and TldrawUiGrid organize UI controls with proper spacing and orientation-aware tooltips.
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.
TldrawUiKbd displays a keyboard shortcut using platform-specific symbols. It is hidden on small mobile breakpoints unless you pass visibleOnMobileLayout:
<TldrawUiKbd>cmd+shift+d</TldrawUiKbd>
TldrawUiSlider is a slider control with discrete integer steps rather than a continuous range:
<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 shows content next to a trigger element:
<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.
TldrawUiDropdownMenuRoot and its companions build a dropdown menu on Radix UI:
<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.