Design system
Dodrawing tokens, primitives, and patterns. Everything on this page is interactive: these are the same components used by the editor, and their values come from the code that builds them.
Principles
Six rules behind most decisions in the system.
Code is the source of truth
Every value on this page comes from code rather than a separate style guide. A smoke test (`scripts/dls-dom.tsx`) compares this documentation with `index.css`, `tailwind.config.ts`, and the real components, so changes to the DLS cannot leave the docs behind.
One token per decision, not per color
There is no “Dodrawing blue.” Use `primary` for actions, `destructive` for destructive actions, `accent` for hover, and `muted` for subdued fills. A theme changes token values, never a list of conditionals.
Height before padding
Controls use fixed heights (`h-9`, `h-8`, `h-7`...). This keeps chips, badges, and panels in the same visual family without tuning each control’s padding.
One pattern for each intent
One primary action per screen, bordered cards, grouped menus, and shortcuts on the right. When a new pattern duplicates an existing one, reuse the existing pattern.
Accessible by default
Focus rings, state retention, and focus restoration are built into base components. A `Button` works with a keyboard without special accessibility props.
Empty states are part of the pattern
Loading, empty, and error states have dedicated components (`Skeleton`, `EmptyState`). No screen stays blank while waiting for data.
Color
HSL variables work consistently in light and dark themes. Components never hard-code colors.
Values are shown exactly as defined in src/index.css. The theme changes by toggling the dark class on documentElement and storing dodrawing.theme in localStorage.
--background
Page background. `body` always uses this token, so changing it affects the whole app.
light 0 0% 100%
dark 240 10% 4%
--foreground
Default text color. It also colors selected text through `::selection` at 15%.
light 240 10% 4%
dark 0 0% 98%
--surface
Cards, panels, and dialogs: the base content surface.
light 0 0% 100%
dark 240 8% 6%
--surface-raised
One level above: tooltips, menus, and card hover states.
light 240 5% 98%
dark 240 7% 9%
--border
Lines and dividers. `*` applies this to every element, so it does not need to be repeated.
light 240 6% 90%
dark 240 5% 17%
--input
Field and checkbox borders. In dark mode this is lighter than `border` to distinguish controls.
light 240 6% 90%
dark 240 5% 20%
--ring
Focus ring. `:focus-visible` applies it with `ring-2` and a 2px offset.
light 240 5% 10%
dark 240 5% 84%
--primary
Primary action. It is nearly black in light mode and nearly white in dark mode, keeping the button clear in both.
light 240 6% 10%
dark 0 0% 98%
--primary-foreground
Text and icons on `primary`. Always the inverse of the token.
light 0 0% 98%
dark 240 6% 10%
--secondary
Secondary actions, chips, and avatar fills.
light 240 5% 96%
dark 240 5% 14%
--secondary-foreground
Text on `secondary`.
light 240 6% 10%
dark 0 0% 98%
--muted
Subdued fill for tabs, segmented controls, and menu hover states.
light 240 5% 96%
dark 240 5% 13%
--muted-foreground
Secondary text and placeholders. In dark mode it rises from 46% to 62% to maintain contrast.
light 240 4% 46%
dark 240 5% 62%
--accent
Button and row hover state. This is not a brand color; it signals “you are here.”
light 240 5% 94%
dark 240 5% 16%
--accent-foreground
Text on `accent`.
light 240 6% 10%
dark 0 0% 98%
--destructive
Delete, undo, and destructive actions. The only token with high chroma.
light 0 72% 51%
dark 0 63% 50%
--destructive-foreground
Text on `destructive`.
light 0 0% 98%
dark 0 0% 98%
States outside the token system
success and warning are not tokens. They only appear in a badge or a canvas marker, never on a large surface, so they use Tailwind colors with 10% fill.
- successemerald`bg-emerald-500/10` + `text-emerald-600` (light) / `text-emerald-400` (dark).
- warningamber`bg-amber-500/10` + `text-amber-600` (light) / `text-amber-400` (dark).
- destructivedestructiveWhen using the token: `bg-destructive/10` + `text-destructive`.
- neutralmuted / accent`bg-muted text-muted-foreground` or `bg-accent text-accent-foreground`.
Typography
Outfit for the interface and Poppins for headings, with system fallbacks.
font-sans
Map the flow of an idea
Outfit, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif
The interface’s geometric typeface. Poppins is reserved for headings.
font-display
Map the flow of an idea
Poppins, ui-sans-serif, system-ui, sans-serif
Typeface for page and section headings and h1–h6 text styles.
font-mono
Map the flow of an idea
ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
Values, IDs, menu shortcuts, and any text that must not wrap.
Scale
Below text-2xs the design system uses arbitrary sizes because the canvas and layer tree need to show a lot of information in a small space:
text-[11px] (Inspector fine print.) · text-[10px] (`<Kbd>`, status badges, and `sm` avatars.) · text-[9px] (`xs` avatars and canvas microcopy.) · text-[8px] (Canvas minimum-scale markers.) · text-[7px] (Ruler numbers.)
Weights
Shape and sizing
A single base radius and a height scale set the interface rhythm.
--radius
0.625rem
Base value. Defined only in `:root`; corner radii do not change with the theme.
rounded-lg
var(--radius)
Cards, dialogs, tooltips, menus, and empty states.
rounded-md
calc(var(--radius) - 2px)
Buttons, inputs, badges, tabs, and menu items. The everyday radius.
rounded-sm
calc(var(--radius) - 4px)
Small elements that should not compete with the text.
rounded-[4px]
4px
Checkbox. Nearly square so the check mark stays legible.
rounded-[3px]
3px
Canvas selection outline, inset so it does not cover the node border.
Control heights
Components
The primitives in src/components/ui, all imported from one index.
The primary action. An HTML button with variants, four heights, and a loading state.
Variants
| Name | Classes | When |
|---|---|---|
| primary | bg-primary text-primary-foreground | hover `/90`, active `/80`, with `shadow-sm`. Use once per screen. |
| secondary | bg-secondary text-secondary-foreground | hover `/70`. |
| ghost | no fill | Only `hover:bg-accent hover:text-accent-foreground`. |
| outline | border border-border bg-transparent | Explicit border; for toolbar actions. |
| danger | bg-destructive text-destructive-foreground | hover `/90`. |
| link | text-foreground underline-offset-4 | No background or fixed height; underlined on hover. |
Sizes
| Name | Classes |
|---|---|
| sm | h-8 px-3 text-xs gap-1.5 |
| md | h-9 px-4 text-sm gap-2 |
| lg | h-11 px-6 text-sm gap-2 |
| icon | h-9 w-9 p-0 |
Props
| Prop | Type | Note |
|---|---|---|
| variant | 'primary' | 'secondary' | 'ghost' | 'outline' | 'danger' | 'link' | Defaults to `primary`. |
| size | 'sm' | 'md' | 'lg' | 'icon' | Defaults to `md`. |
| loading | boolean | Replaces the content with a spinner in the text color while preserving the width. |
Details
- Base classes are `inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors duration-150`; disabled state uses `disabled:pointer-events-none disabled:opacity-50`.
- `buttonStyles({ variant, size, className })` returns the same classes for use on a `<Link>`, keeping styling consistent. The public navigation uses this for its CTAs.
- The spinner uses `h-3.5 w-3.5 animate-spin border-[1.5px] border-current border-t-transparent`. It inherits the text color, so it works with every variant.
Accessibility
- It is a native `<button>`, so it inherits disabled behavior, focus, and activation with Enter or Space.
Live example
Variants
Sizes
Loading and disabled
buttonStyles on a Link
Without the componentsrc/components/ui/Button.tsx
Layers
Stacking order, from highest to lowest.
Motion
Five animations, two durations, and a global prefers-reduced-motion override.
- Two durations only: `duration-150` for hover and focus, and `duration-200` for the header on scroll. Controls use no long transitions.
- Color changes use `transition-colors`; position changes use `transition-transform`. `transition-all` is reserved for pressing a canvas tile.
- Everything is disabled by `prefers-reduced-motion: reduce`: `index.css` sets animation and transition to 0.01ms and removes smooth scrolling.
- Tooltips and menus use `animate-scale-in` instead of a fade, so they feel elevated rather than like a flat state change.
Shortcuts
Generated from src/lib/shortcuts.ts, using the current platform symbol.
Global
Navigation
Editor
Vista
Canvas navigation
Inside a text field, only Escape, Enter and shortcuts using mod or alt. The editor icons V H F select, pan, and frame content.
Accessibility
Built-in accessibility, without extra props.
Always-visible focus
`:focus-visible` applies `outline-none ring-2 ring-ring ring-offset-2 ring-offset-background`. The browser outline is removed while the ring remains visible, including on `primary`.
The real element holds the state
`Checkbox` visually hides its input with `sr-only`, and `Dialog` renders its overlay as a `div`. In both cases the DOM keeps the semantics separate from the visuals.
Real labels
`Input` links its `label` by `id` instead of relying on a placeholder. `Avatar` uses `name` as `alt`, and `Tooltip` uses `aria-describedby` instead of `title`.
Focus restoration
`Dialog` stores the active element when opened and restores it when closed, so `Escape` returns the user to where they were.
Scrolling is locked while a modal is open
`Dialog` sets `overflow: hidden` on `body` while open and restores it when closed, including if it unmounts while open.
Motion follows system settings
`@media (prefers-reduced-motion: reduce)` sets animations and transitions to 0.01ms and disables smooth scrolling. Components do not need to opt in individually.
Shortcuts adapt to each platform keyboard
`formatChord` maps `mod` to `⌘` on Mac and `Ctrl` elsewhere. `eventMatchesChord` ignores shortcuts inside text fields except `Escape`, `Enter`, and combinations using `mod` or `alt`.
Tabular numbers where values change
Tab counters, inspector measurements, and prices use `tabular-nums` so columns stay aligned as values change.
Editor
Canvas gestures and the rules behind them.
- Isolation uses a single scope, not a stack. Returning to the overview does not restore the previous scope.
- Outside the active scope, nodes cannot be moved, resized, edited, or opened in a context menu; pointer events pass through.
- While editing, text is tinted and outlined above the node frame so the selection remains visible.
- Any element can contain editable text, whether or not it has children. In a container, text sits at the top left like its visible title, with pointer events disabled so it does not cover children.
- A container only renders its text layer while it is being edited or when its text differs from its name; otherwise, the name would appear twice.
- Double-click opens only the actual text editor (`data-text-hit`). Press `Enter` to edit a container title.
Panels
The workspace and its dockable panels.
- Side docks are horizontal; top and bottom docks are vertical.
- The toolbar stays against the edge and does not shrink with the other panels.
- A dock must be at least 120 px wide; below that, its contents are not usable.
- The bottom bar is permanent and sits outside the docks, so it does not depend on available panel space.
- Sizes are stored in `dodrawing.panel.sizes.v2`; visible docks are stored in `dodrawing.panel.docks.v1`.