Toasts and dialogs

Add, remove and clear toasts and dialogs in your app using the useToasts and useDialogs hooks.

import { Select as _Select } from 'radix-ui'
import { useState } from 'react'
import {
	TLComponents,
	Tldraw,
	TldrawUiButton,
	TldrawUiButtonLabel,
	TldrawUiDialogBody,
	TldrawUiDialogCloseButton,
	TldrawUiDialogFooter,
	TldrawUiDialogHeader,
	TldrawUiDialogTitle,
	useContainer,
	useDialogs,
	useToasts,
} from 'tldraw'
import 'tldraw/tldraw.css'

// There's a guide at the bottom of this file

// [1]
function MyDialog({ onClose }: { onClose(): void }) {
	return (
		<>
			<TldrawUiDialogHeader>
				<TldrawUiDialogTitle>Title</TldrawUiDialogTitle>
				<TldrawUiDialogCloseButton />
			</TldrawUiDialogHeader>
			<TldrawUiDialogBody style={{ maxWidth: 350 }}>Description...</TldrawUiDialogBody>
			<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
				<TldrawUiButton type="normal" onClick={onClose}>
					<TldrawUiButtonLabel>Cancel</TldrawUiButtonLabel>
				</TldrawUiButton>
				<TldrawUiButton type="primary" onClick={onClose}>
					<TldrawUiButtonLabel>Continue</TldrawUiButtonLabel>
				</TldrawUiButton>
			</TldrawUiDialogFooter>
		</>
	)
}

// [2]
function MySimpleDialog({ onClose }: { onClose(): void }) {
	return (
		<div style={{ padding: 16 }}>
			<h2>Title</h2>
			<p>Description...</p>
			<button onClick={onClose}>Okay</button>
		</div>
	)
}

// [3]
function MyDialogWithSelect({ onClose }: { onClose(): void }) {
	const container = useContainer()
	const [value, setValue] = useState('a')
	return (
		<>
			<TldrawUiDialogHeader>
				<TldrawUiDialogTitle>Dialog with a select</TldrawUiDialogTitle>
				<TldrawUiDialogCloseButton />
			</TldrawUiDialogHeader>
			<TldrawUiDialogBody style={{ maxWidth: 350 }}>
				<p>A select opened inside a modal is its own dismissable layer.</p>
				<_Select.Root value={value} onValueChange={setValue}>
					<_Select.Trigger
						data-testid="dialog-select.trigger"
						style={{ display: 'flex', alignItems: 'center', gap: 8 }}
					>
						<_Select.Value />
						<_Select.Icon></_Select.Icon>
					</_Select.Trigger>
					<_Select.Portal container={container}>
						<_Select.Content
							data-testid="dialog-select.content"
							position="popper"
							sideOffset={4}
							style={{
								backgroundColor: 'var(--tl-color-panel)',
								boxShadow: 'var(--tl-shadow-3)',
								borderRadius: 'var(--tl-radius-2)',
								padding: 4,
								zIndex: 'var(--tl-layer-canvas-overlays)',
							}}
						>
							<_Select.Viewport>
								{['a', 'b', 'c'].map((v) => (
									<_Select.Item
										key={v}
										value={v}
										data-testid={`dialog-select.item-${v}`}
										style={{ padding: '4px 8px', cursor: 'pointer' }}
									>
										<_Select.ItemText>Option {v}</_Select.ItemText>
									</_Select.Item>
								))}
							</_Select.Viewport>
						</_Select.Content>
					</_Select.Portal>
				</_Select.Root>
			</TldrawUiDialogBody>
			<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
				<TldrawUiButton type="primary" onClick={onClose}>
					<TldrawUiButtonLabel>Done</TldrawUiButtonLabel>
				</TldrawUiButton>
			</TldrawUiDialogFooter>
		</>
	)
}

// [4] A dialog that opens a second dialog, so the two stack. Stacked dialogs stay modal,
// so each one — including the topmost — keeps its own controls interactive (taps included).
function MyNestedDialog({ onClose }: { onClose(): void }) {
	const { addDialog } = useDialogs()
	return (
		<div data-testid="dialog-parent" style={{ padding: 16 }}>
			<h2>Parent dialog</h2>
			<p>Opens another dialog on top of itself.</p>
			<button
				data-testid="dialog-parent.open-nested"
				onClick={() => addDialog({ component: MyConfirmDialog })}
			>
				Open nested dialog
			</button>
			<button onClick={onClose}>Close</button>
		</div>
	)
}

function MyConfirmDialog({ onClose }: { onClose(): void }) {
	return (
		<div data-testid="dialog-nested" style={{ padding: 16 }}>
			<h2>Nested dialog</h2>
			<button data-testid="dialog-nested.confirm" onClick={onClose}>
				Confirm
			</button>
		</div>
	)
}

const CustomSharePanel = () => {
	const { addToast } = useToasts()
	const { addDialog } = useDialogs()

	return (
		<div style={{ padding: 16, gap: 16, display: 'flex', pointerEvents: 'all' }}>
			<button
				onClick={() => {
					addToast({ title: 'Hello world!', severity: 'success' })
				}}
			>
				Show toast
			</button>
			<button
				data-testid="show-dialog"
				onClick={() => {
					addDialog({
						component: MyDialog,
						onClose() {
							// You can do something after the dialog is closed
							void null
						},
					})
				}}
			>
				Show dialog
			</button>
			<button
				onClick={() => {
					addDialog({
						component: MySimpleDialog,
						onClose() {
							// You can do something after the dialog is closed
							void null
						},
					})
				}}
			>
				Show simple dialog
			</button>
			<button
				data-testid="show-dialog-with-select"
				onClick={() => {
					addDialog({
						component: MyDialogWithSelect,
						onClose() {
							// You can do something after the dialog is closed
							void null
						},
					})
				}}
			>
				Show dialog with select
			</button>
		</div>
	)
}

// Rendered in front of the canvas rather than in the SharePanel, which overflows
// off-screen on mobile — so the stacked-dialog demo stays reachable on a touchscreen.
function StackedDialogLauncher() {
	const { addDialog } = useDialogs()
	return (
		<button
			data-testid="show-nested-dialog"
			style={{
				position: 'absolute',
				top: '50%',
				left: 8,
				transform: 'translateY(-50%)',
				pointerEvents: 'all',
			}}
			onClick={() => addDialog({ component: MyNestedDialog })}
		>
			Show nested dialog
		</button>
	)
}

const components: TLComponents = {
	SharePanel: CustomSharePanel,
	InFrontOfTheCanvas: StackedDialogLauncher,
}

export default function ToastsDialogsExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw components={components} persistenceKey="example" />
		</div>
	)
}

/*

To control toasts and dialogs your app, you can use the `useToasts` and `useDialogs` hooks.
These hooks give you access to functions which allow you to add, remove and clear toasts
and dialogs.

Dialogs are especially customisable, allowing you to pass in a custom component to render
as the dialog content. Alternatively, you can use the `ExampleDialog` component which is
provided by the library.

[1]
The tldraw library provides a set of components that you can use to build your dialogs.
The `onClose` function passed to the dialog component runs when the dialog closes or
is dismissed, but you can also call it from buttons to close the dialog.

[2]
...or you can build your own dialog component!

[3]
Dialogs can contain their own popups, like a select menu. Because tldraw's dialog uses
Radix's dismissable layers, clicking outside an open select closes just the select and
leaves the dialog open — a second outside click then closes the dialog.
*/

To control toasts and dialogs your app, you can use the useToasts and useDialogs hooks. These hooks give you access to functions which allow you to add, remove and clear toasts and dialogs.

Dialogs are especially customisable, allowing you to pass in a custom component to render as the dialog content. Alternatively, you can use the ExampleDialog component which is provided by the library.

Is this page helpful?
Prev
Things on the canvas
Next
UI primitives