Back to Tldraw

UI primitives

apps/docs/content/sdk-features/ui-primitives.mdx

5.4.014.4 KB
Original Source

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.

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:

TypeDescription
normalStandard button for general actions
primaryEmphasized button for primary actions
dangerRed button for destructive actions
lowSubtle button with minimal visual weight
iconSquare button sized for a single icon
toolTool button style used in the toolbar
menuButton style used inside menus
helpStyle 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" />

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

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:

PropDescription
idUnique identifier for the menu item
labelDisplay text (supports translation keys)
iconIcon for icon-style menus and toolbars (not shown in dropdown or context menus)
iconLeftIcon shown on the left in dropdown and context menus
kbdKeyboard shortcut to display
onSelectCalled when the item is clicked
disabledWhether the item is disabled
readonlyOkIf true, item is shown even in readonly mode
isSelectedWhether the item shows as selected (for toolbar items)
spinnerShow a loading spinner
noClosePrevent 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:

PropDescription
titleTitle text, combined with the label for the tooltip and aria-label
labelTranslation key for the label
valueCurrent value (min to steps), or null
stepsMaximum value
minOptional minimum value (defaults to 0)
onValueChangeCalled with the new value when it changes
onHistoryMarkCalled 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.