All documents

Overlays, and the primitives not covered elsewhere

All 31 primitives ship in components/ui/. Buttons, inputs, selects, switches, badges and the table have their own documents; this is everything else, with the rule that governs each.

Dialog

Centred, max-w-lg (calc(100% - 2rem) on a phone), 8 px radius, 24 px padding, 16 px internal gap, --popover fill, shadow-modal (the top elevation tier). The close button is a 44 px target in the corner with a 16 px glyph.

Use a dialog for a decision that must be made now. Never for a form with more than a handful of fields — that is a screen. Never nest one inside another.

Sheet

The same content on a slide-in panel: 75% width capped at sm from the side, full width from top or bottom, --popover fill, shadow-modal, a border on the edge it is attached to. Right is the default.

Use a sheet for a detail view beside the thing that opened it, and for the mobile rail (the sidebar primitive already does this — you do not wire it).

Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis — the header's "where you are". The last step is a BreadcrumbPage, never a link.

Popover and dropdown menu

--popover fill, shadow-overlay (one tier below modal), 8 px radius, 4 px padding, items at 6 px radius with the accent tint on focus. Menu items take a 16 px leading icon and 8 px gap.

variant="destructive" on a menu item is for the one irreversible action, and it goes under a separator.

The full set: DropdownMenu, DropdownMenuPortal, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent.

Collapsible

Collapsible, CollapsibleTrigger, CollapsibleContent — a disclosure. In the rail it turns a nav row into an expandable group, and there the trigger is its OWN 20px control rather than the row: clicking the label must still navigate, so one target means one action. A group opens on its own when the current path is inside it.

Tabs

Two variants, and line is the default: transparent list, 4 px gaps, the active tab marked by a 2 px bar in --foreground under it. default is the enclosed pill set on --muted, 36 px tall — use it only inside a card where a line would compete with the card's own edge.

Tabs are for views of one thing. The moment a tab is a different subject, it is a route (this is why "How it works" left the admin screen and became a documentation page).

HoverCard

HoverCard, HoverCardTrigger, HoverCardContent — a PREVIEW, where a tooltip would be a paragraph.

The line between them is the 160-character cap: a tooltip is one or two sentences a reader takes in at a glance, so anything with several FIELDS in it — a county's FIPS, its homes, its metro — needs this instead. Same popover surface and overlay elevation; the difference is that this one holds structure.

Not for anything a reader has to act on: it is dismissed by moving the mouse.

ConfirmDialog

The one thing every destructive Button needs to open. It names what will happen in its title, keeps the irreversible action as the primary, and carries its own pending state — so a slow delete does not look like a dead button.

Never for a decision that is reversible: a confirm on a safe action teaches people to click through confirms.

Field

Field, FieldLabel, FieldDescription, FieldError, plus FieldGroup, FieldSet, FieldLegend, FieldSeparator, FieldContent, FieldTitle.

A label, a control, its help text and its error, wired together for accessibility. Pairing a Label and an Input by hand works right up until the error text, which then has nothing pointing at it and is never announced. Use this for any form field that can fail.

Checkbox and Textarea

Checkbox is a boolean that waits for Save; a Switch is a boolean that applies immediately. That is the whole distinction and it decides which one.

Textarea is the multi-line field. It exists so a fourth hand-rolled copy does not.

Progress and InlineSpinner

Progress is a determinate bar for a job with a known end — an upload, a quota, a queue — and it is always beside its figure: a bar with no number is a guess dressed as a measurement. It is not Metric's progress, which is a 5px track against a target.

InlineSpinner is the indeterminate one, at three sizes, for inside a button or a row.

InlineEmpty

InlineEmpty / InlineEmptyProps — the compact empty, for a slot INSIDE something: a chart with no data, a table body, a panel. The full EmptyState is a Card, and a Card inside a Card is what the layout rules forbid.

Toaster

Transient confirmation, from sonner — and it has exactly one job: something succeeded and needs no action. "Copied." "Link sent."

A failure never goes in a toast. It disappears and takes the only record of what went wrong with it; failures are an Alert on the screen, beside the thing that failed. If the reader has to do anything about it, it is not a toast.

Tooltip and InfoHint

InfoHint is the (?): a tooltip trigger with a real accessible name, for column headers and stat labels. Keep the body to one or two sentences and cap it at ~42 characters wide.

Never put information a reader needs to ACT in a tooltip.

Slider, Toggle, ToggleGroup

  • Slider for a value where the shape matters more than the number, and it always shows the number beside it.
  • Toggle / ToggleGroup for 2–4 mutually exclusive views. More than four is a Select; two that are on/off is a Switch. A group carries its own 1px edge and the items join into one segmented control; selected is an ink rule rather than a deeper fill. Both rules, and the third control in the family (CountFilter), are in 04-buttons-and-controls.md.

MultiSelectFilter

A dropdown of checkboxes over a noun ("3 counties"), with a clear-all. Use it when a filter has more options than a ToggleGroup can hold. The trigger states the count and the noun, never a list of truncated labels.

StatusDot

A 8 px dot in one of four tones — positive, caution, negative, neutral — optionally with a label. It is the smallest state device in the system: use it in a dense row where a badge would be too loud, and never as the only cue (the label beside it carries the meaning for anyone who cannot see the colour).

KpiCard

The card-shaped figure: eyebrow, value, optional footer and info. StatTile in components/brand/devices.tsx is the leaner version and the one to reach for first; KpiCard earns its extra weight when the tile needs a footer, an action or a loading skeleton.

TablePagination

Page sizes 10 / 25 / 50, first / prev / next / last as icon-sm ghost buttons, and the range stated in words ("26–50 of 1,894"). 44 px tall — it is part of the table's height budget (07-tables-and-data.md).

Skeleton, Avatar, Separator, Label

  • Skeleton: shaped like the thing that is coming. Two tokens (--skeleton, --skeleton-pulse), so it is never a grey block on a warm page.
  • Avatar: round, two-letter fallback, and referrerPolicy="no-referrer" on any photo from a provider that 403s an unexpected referer — otherwise Radix shows the fallback and it reads as "no photo".
  • Separator: --border. A rule separates; it never decorates.
  • Label: pairs with an input by htmlFor. Every input has one, even when the design hides it (sr-only).

AnimatedIcon

A 200 ms rotate-and-scale on hover or focus, motion-safe only, with a group trigger so a whole tile can drive it. Reserved for something that goes somewhere — a tile that links, a row that opens. Motion on a static icon is noise.