# Orbit DS > The shared design system for Orbit B2B and admin applications. Code-first, strongly > tokenised, built on shadcn and Base UI, and tuned for dense admin software. > 87 components: 44 primitives and 43 patterns. Layers, and the dependency direction that is never violated: ``` tokens <- ui <- patterns <- apps icons <- patterns <- apps brand <- apps ``` - **@orbit/ui** - generic primitives. One concept each. Nothing that knows about a domain. - **@orbit/patterns** - compositions every admin app needs. Presentational; state comes from outside. - **@orbit/tokens** - the source of truth for every colour, space and radius. - **@orbit/icons** - a curated Lucide re-export, for patterns and apps. Rules that hold everywhere: - No raw hex, no `rgb()`, no Tailwind palette classes. Use a semantic role: `bg-card`, `text-text-muted`, `border-border`, `bg-success-bg`. - No arbitrary spacing. Use `px-cell`, `h-row`, `p-card`, `p-page`, `gap-stack`, or Tailwind's 4px scale. - Theme and density are runtime axes: `.dark` and `data-density="cozy"` on ``. Compact is the default. Never write either yourself - use ThemeProvider and DensityProvider. - Status colours come in triads: `--success` is the hue, `--success-bg` the tint, `--success-on-bg` the text on that tint. Text on a tint uses `-on-bg`, never the hue. - Never a dual-axis chart. The chart palette is capped at four series and `--success`, `--warning` and `--destructive` are reserved for status. - Every interactive control needs an accessible name; an icon is not a name. Status is never conveyed by colour alone. ## Install Requirements: React ^19 (components use ref-as-prop; React 18 is not supported), Tailwind v4, Node ^20.19 || >=22.12 (Vite 8's floor), TypeScript ~5.9+. ```bash npx shadcn@latest add @orbit/tokens @orbit/button @orbit/resource-table ``` Configure the namespace once, then install `@orbit/tokens` first and import it from your stylesheet after Tailwind: ```json { "registries": { "@orbit": "https://ds.orbit.bhurley.dev/r/{name}.json" } } ``` Your entry stylesheet is exactly three lines - the tokens file carries the fonts, the Nova preset, the `dark` custom variant, the default border colour and the body ground, so do NOT add a base layer or theme variables of your own: ```css @import "tailwindcss"; @import "./styles/orbit-tokens.css"; @source "./**/*.{ts,tsx}"; ``` Mount the runtime axes once, above everything (`@orbit/theme-provider` and `@orbit/density-provider` install both): ```tsx import { StrictMode } from "react" import { createRoot } from "react-dom/client" import "./index.css" import { App } from "./App" import { DensityProvider } from "@/components/ui/density-provider" import { ThemeProvider } from "@/components/ui/theme-provider" createRoot(document.getElementById("root")!).render( ) ``` Every app then wraps its screens in AppShell and puts `` (`@orbit/theme-toggle`) in `AppTopbar` `actions` - without it the reader has no way to change theme. Never write the `dark` class or `data-density` yourself; the providers own both. The public registry host is ds.orbit.bhurley.dev. It serves this file, llms-full.txt, catalog.json and /r/ without authentication. The documentation pages require application-level sign-in on the same host. Set ORBIT_REGISTRY_ORIGIN when building for your host. ## Structured data - [llms-full.txt](/llms-full.txt): the full expanded catalog - every component's props, variants, guidance, anti-patterns and example, inline. - [catalog.json](/catalog.json): every component with its props, variants, guidance and dependencies. - [registry.json](/r/registry.json): the shadcn registry index. - Lunar texture plates for `LunarSurface`'s `texture` prop, hosted beside the registry: [/assets/orbit-lunar/orbit-texture.svg](/assets/orbit-lunar/orbit-texture.svg), [/assets/orbit-lunar/orbit-texture.svg](/assets/orbit-lunar/orbit-texture.svg), [/assets/orbit-lunar/orbit-texture.svg](/assets/orbit-lunar/orbit-texture.svg). Copy them into your app or hotlink; the pure-CSS lunar needs no asset (`--grad-lunar`). ## Layout - [App shell](/components/patterns/app-shell) (pattern, stable): AppShell is the page frame for every Orbit admin app: dark rail, sticky topbar, page body. Import from `@orbit/patterns/patterns/app-shell`. USE WHEN: Every authenticated Orbit screen. Wrap the app in it once. PREFERRED OVER: sidebar. A11Y: Opens with a skip link, the first tab stop, so the rail can be bypassed. One main landmark, a labelled nav for the rail, and every nav list named by its heading. PARTS: [AppSidebar](/components/patterns/app-shell#app-sidebar), [AppNavGroup](/components/patterns/app-shell#app-nav-group), [AppNavItem](/components/patterns/app-shell#app-nav-item), [AppBrand](/components/patterns/app-shell#app-brand), [AppMain](/components/patterns/app-shell#app-main), [AppTopbar](/components/patterns/app-shell#app-topbar), [AppPage](/components/patterns/app-shell#app-page). - [Card](/components/card) (primitive, stable): Card surface for Orbit DS - a hairline border with cool-tinted elevation. Import from `@orbit/ui/components/card`. USE WHEN: Content is a discrete object or a grouped interaction. NOT WHEN: You are separating sections of prose - use whitespace and a heading. AVOID: Do NOT nest a Card inside a Card - two borders and two shadows read as a bug. Use a `DetailSection` or a plain heading to subdivide one instead. USE INSTEAD: metric-card. RELATED: lunar-surface. COMPOSED WITH: avatar, badge, board, button, gradient-orb, lunar-surface, status-badge. A11Y: A plain container with no role. If the card is a section of the page, give it a real heading rather than styled text. PARTS: [CardHeader](/components/card#card-header), [CardTitle](/components/card#card-title), [CardDescription](/components/card#card-description), [CardAction](/components/card#card-action), [CardContent](/components/card#card-content), [CardFooter](/components/card#card-footer). - [Item](/components/item) (primitive, beta): Item primitives for Orbit DS - a generic media/content/actions row. Import from `@orbit/ui/components/item`. USE WHEN: A media, content and actions row inside a list. NOT WHEN: The rows are tabular and comparable - that is Table. COMPOSED WITH: badge, button. A11Y: Render as an li inside an ItemGroup, which carries role="list", so the count is announced. PARTS: [ItemGroup](/components/item#item-group), [ItemSeparator](/components/item#item-separator), [ItemMedia](/components/item#item-media), [ItemContent](/components/item#item-content), [ItemTitle](/components/item#item-title), [ItemDescription](/components/item#item-description), [ItemActions](/components/item#item-actions), [ItemHeader](/components/item#item-header), [ItemFooter](/components/item#item-footer). - [Page header](/components/patterns/page-header) (pattern, stable): PageHeader is the standard title block at the top of every Orbit admin page. Import from `@orbit/patterns/patterns/page-header`. USE WHEN: The top of any Orbit page. COMPOSED WITH: button. A11Y: Renders the page's only h1 by default. Pass as="h2" when the page already has one. - [Separator](/components/separator) (primitive, stable): Separator primitive for Orbit DS. Import from `@orbit/ui/components/separator`. USE WHEN: A rule genuinely divides content whose grouping matters. NOT WHEN: You are reaching for rhythm - use whitespace. COMPOSED WITH: button. A11Y: Decorative by default and aria-hidden, because a rule that exists for rhythm should not be announced. - [Sheet](/components/sheet) (primitive, stable): Sheet is the side-panel overlay primitive, built on Base UI Dialog. Import from `@orbit/ui/components/sheet`. USE WHEN: A task needs an edge panel without leaving the page. NOT WHEN: You are inspecting one record beside its list - that is DetailDrawer. AVOID: Do NOT open a sheet from inside another sheet. USE INSTEAD: detail-drawer. RELATED: collapsible, detail-drawer. COMPOSED WITH: button. A11Y: Traps focus, returns it to the trigger on close, and closes on Escape - all from Base UI. Needs a SheetTitle for its accessible name. PARTS: [SheetTrigger](/components/sheet#sheet-trigger), [SheetClose](/components/sheet#sheet-close), [SheetContent](/components/sheet#sheet-content), [SheetHeader](/components/sheet#sheet-header), [SheetFooter](/components/sheet#sheet-footer), [SheetTitle](/components/sheet#sheet-title), [SheetDescription](/components/sheet#sheet-description). - [Sidebar](/components/sidebar) (primitive, stable): Sidebar primitives for Orbit DS, from the shadcn base-nova registry. Import from `@orbit/ui/components/sidebar`. USE WHEN: Building a shell that is not AppShell. NOT WHEN: You are building a Orbit app - use AppShell. USE INSTEAD: app-shell. A11Y: Wrap the rail's content in a labelled nav so it can be jumped to; AppShell does this for you. Give each menu list its heading as an accessible name, or it is announced as an unnamed list of N items. A collapsed rail hides labels, so every button needs an aria-label that survives the collapse. PARTS: [SidebarProvider](/components/sidebar#sidebar-provider), [SidebarTrigger](/components/sidebar#sidebar-trigger), [SidebarRail](/components/sidebar#sidebar-rail), [SidebarInset](/components/sidebar#sidebar-inset), [SidebarInput](/components/sidebar#sidebar-input), [SidebarHeader](/components/sidebar#sidebar-header), [SidebarFooter](/components/sidebar#sidebar-footer), [SidebarSeparator](/components/sidebar#sidebar-separator), [SidebarContent](/components/sidebar#sidebar-content), [SidebarGroup](/components/sidebar#sidebar-group), [SidebarGroupLabel](/components/sidebar#sidebar-group-label), [SidebarGroupAction](/components/sidebar#sidebar-group-action), [SidebarGroupContent](/components/sidebar#sidebar-group-content), [SidebarMenu](/components/sidebar#sidebar-menu), [SidebarMenuItem](/components/sidebar#sidebar-menu-item), [SidebarMenuButton](/components/sidebar#sidebar-menu-button), [SidebarMenuAction](/components/sidebar#sidebar-menu-action), [SidebarMenuBadge](/components/sidebar#sidebar-menu-badge), [SidebarMenuSkeleton](/components/sidebar#sidebar-menu-skeleton), [SidebarMenuSub](/components/sidebar#sidebar-menu-sub), [SidebarMenuSubItem](/components/sidebar#sidebar-menu-sub-item), [SidebarMenuSubButton](/components/sidebar#sidebar-menu-sub-button). ## Navigation - [Breadcrumb](/components/breadcrumb) (primitive, stable): Shows where the current page sits in the site hierarchy, as a nav landmark. Import from `@orbit/ui/components/breadcrumb`. USE WHEN: A page sits inside a hierarchy the reader may want to climb. NOT WHEN: The hierarchy is one level deep, or you mean a back button. A11Y: A nav landmark labelled "breadcrumb", so it can be jumped to. Only the current page carries aria-current, and it is not a link. PARTS: [BreadcrumbList](/components/breadcrumb#breadcrumb-list), [BreadcrumbItem](/components/breadcrumb#breadcrumb-item), [BreadcrumbLink](/components/breadcrumb#breadcrumb-link), [BreadcrumbPage](/components/breadcrumb#breadcrumb-page), [BreadcrumbSeparator](/components/breadcrumb#breadcrumb-separator), [BreadcrumbEllipsis](/components/breadcrumb#breadcrumb-ellipsis). - [Collapsible](/components/collapsible) (primitive, stable): Shows and hides a region from a trigger, animating its measured height. Import from `@orbit/ui/components/collapsible`. USE WHEN: Optional detail the reader can ignore. NOT WHEN: The content is required to finish the task. AVOID: Do NOT hide anything required to complete a task, and never collapse the only copy of an error message - people do not open panels to look for problems they have not been told about. RELATED: sheet. COMPOSED WITH: button. A11Y: The trigger carries aria-expanded and is associated with the panel by Base UI. The panel unmounts while closed, so in-page search cannot reach it. Pass hiddenUntilFound where it should. PARTS: [CollapsibleTrigger](/components/collapsible#collapsible-trigger), [CollapsibleContent](/components/collapsible#collapsible-content). - [Pagination](/components/pagination) (primitive, stable): Pagination primitives for Orbit DS. Import from `@orbit/ui/components/pagination`. USE WHEN: A collection is longer than one page and the reader may jump around. NOT WHEN: The list is short enough to show at once. RELATED: resource-table. A11Y: A nav landmark. The current page carries aria-current, and previous/next have accessible names even when they show only an icon. PARTS: [PaginationContent](/components/pagination#pagination-content), [PaginationItem](/components/pagination#pagination-item), [PaginationLink](/components/pagination#pagination-link), [PaginationPrevious](/components/pagination#pagination-previous), [PaginationNext](/components/pagination#pagination-next), [PaginationEllipsis](/components/pagination#pagination-ellipsis). - [Segmented control](/components/patterns/segmented-control) (pattern, stable): SegmentedControl filters or changes the view of ONE dataset - a time range, list vs grid. Import from `@orbit/patterns/patterns/segmented-control`. USE WHEN: Switching the view of one dataset: a range, a unit, list versus grid. NOT WHEN: The options swap genuinely different panels - that is Tabs. PREFERRED OVER: tabs, toggle-group. RELATED: stepper, tabs. COMPOSED WITH: chart, chart-card, table. A11Y: A group of toggle buttons, not a tablist - it filters one dataset rather than swapping panels. The group needs a label; the options alone rarely say what they change. - [Tabs](/components/tabs) (primitive, stable): Tabs primitives for Orbit DS. Use the `line` variant to switch between panels. Import from `@orbit/ui/components/tabs`. USE WHEN: Panels hold genuinely different content. NOT WHEN: You are reshaping one dataset - that is SegmentedControl. USE INSTEAD: segmented-control. RELATED: segmented-control. A11Y: Real tablist semantics: arrow keys move between tabs and one panel is visible at a time. Only use it when the panels hold genuinely different content. Filtering one dataset is a SegmentedControl. PARTS: [TabsList](/components/tabs#tabs-list), [TabsTrigger](/components/tabs#tabs-trigger), [TabsContent](/components/tabs#tabs-content). ## Actions - [Button](/components/button) (primitive, stable): Button primitive for Orbit DS, built on Base UI with the Nova size ladder. Import from `@orbit/ui/components/button`. USE WHEN: Something happens when it is pressed. NOT WHEN: It navigates - render an anchor, so middle-click and open-in-new-tab work. AVOID: Do NOT use a Button for navigation: render an anchor, so middle-click and open-in-new-tab behave. COMPOSED WITH: alert, badge, card, cell, collapsible, confirm-dialog, data-grid, data-grid-column-panel, data-grid-export-menu, data-grid-toolbar, detail-drawer, dialog, dropdown-menu, empty, field, filter-bar, form, gradient-orb, hero, inline-alert, input, input-group, item, kbd, lunar-surface, marketing-nav, page-header, popover, pricing-tier, property-list, resource-table, search-input, select, separator, sheet, status-badge, tooltip. A11Y: An icon-only button needs aria-label. The icon is not a name. Renders a real button, so Enter and Space activate it and it appears in the tab order. The focus ring is never removed; it is a 3px ring that clears the button's own border. - [Dropdown menu](/components/dropdown-menu) (primitive, stable): Dropdown menu primitives built on Base UI Menu - row actions and overflow menus. Import from `@orbit/ui/components/dropdown-menu`. USE WHEN: Several actions share one trigger. NOT WHEN: There are two actions and room for both - show them. AVOID: Do NOT use a menu for navigation between pages, or for a form control that happens to have options - that is a Select. USE INSTEAD: row-actions-menu. COMPOSED WITH: button. A11Y: Real menu semantics: arrow keys move between items, Escape closes, focus returns to the trigger. A destructive item is marked by its label and position, not by colour alone. PARTS: [DropdownMenuPortal](/components/dropdown-menu#dropdown-menu-portal), [DropdownMenuTrigger](/components/dropdown-menu#dropdown-menu-trigger), [DropdownMenuContent](/components/dropdown-menu#dropdown-menu-content), [DropdownMenuGroup](/components/dropdown-menu#dropdown-menu-group), [DropdownMenuLabel](/components/dropdown-menu#dropdown-menu-label), [DropdownMenuItem](/components/dropdown-menu#dropdown-menu-item), [DropdownMenuSub](/components/dropdown-menu#dropdown-menu-sub), [DropdownMenuSubTrigger](/components/dropdown-menu#dropdown-menu-sub-trigger), [DropdownMenuSubContent](/components/dropdown-menu#dropdown-menu-sub-content), [DropdownMenuCheckboxItem](/components/dropdown-menu#dropdown-menu-checkbox-item), [DropdownMenuRadioGroup](/components/dropdown-menu#dropdown-menu-radio-group), [DropdownMenuRadioItem](/components/dropdown-menu#dropdown-menu-radio-item), [DropdownMenuSeparator](/components/dropdown-menu#dropdown-menu-separator), [DropdownMenuShortcut](/components/dropdown-menu#dropdown-menu-shortcut). - [Kbd](/components/kbd) (primitive, stable): A keyboard key, for shortcut hints in tooltips, menus and search affordances. Import from `@orbit/ui/components/kbd`. USE WHEN: A control has a keyboard shortcut worth advertising. NOT WHEN: You are showing a code identifier or a token name - that is `CellMono`. AVOID: Do NOT use a Kbd as the only affordance for an action. A shortcut is an accelerator, never the only way in. COMPOSED WITH: button. A11Y: Renders a real `kbd`, announced as keyboard input. Decorative on its own: the control it hints at must still have an accessible name. - [Row actions menu](/components/patterns/row-actions-menu) (pattern, stable): RowActionsMenu is the trailing overflow menu on a table row. Import from `@orbit/patterns/patterns/row-actions-menu`. USE WHEN: A row has secondary operations that do not deserve a column each. NOT WHEN: There is one action - put it in the row. PREFERRED OVER: dropdown-menu. A11Y: The trigger needs an accessible name that says which row it acts on - "Actions for Checkout API", not "Actions". The destructive item goes last, after a separator, so it is never adjacent to a routine one. PARTS: [RowActionItem](/components/patterns/row-actions-menu#row-action-item). - [Toggle](/components/toggle) (primitive, stable): Toggle primitive for Orbit DS - a single pressed/unpressed control. Import from `@orbit/ui/components/toggle`. USE WHEN: One thing is either on or off and shows its own state. NOT WHEN: Pressing it performs an action rather than holding a state - that is Button. RELATED: checkbox, toggle-group. A11Y: Renders aria-pressed, so its state is announced. An icon-only toggle still needs an aria-label. - [Toggle group](/components/toggle-group) (primitive, stable): Toggle group primitives for Orbit DS. Import from `@orbit/ui/components/toggle-group`. USE WHEN: Several related toggles share one state and combine. NOT WHEN: Exactly one must be selected - that is SegmentedControl. USE INSTEAD: segmented-control. RELATED: toggle. A11Y: A single tab stop with arrow-key navigation between items. The group needs a label saying what the items change. PARTS: [ToggleGroupItem](/components/toggle-group#toggle-group-item). ## Forms - [Calendar](/components/calendar) (primitive, beta): Calendar primitive for Orbit DS - month grids for picking dates and ranges. Import from `@orbit/ui/components/calendar`. USE WHEN: Picking a date or range where the month grid should be visible in place. NOT WHEN: Typing is faster than picking - a birth date decades back is an Input with validation, not a calendar to page through. RELATED: date-range-picker. A11Y: react-day-picker renders a grid of real buttons: arrow keys move between days, and each day carries its full date as an accessible name. Selected, today and range states are conveyed by aria-selected and the day's name, not colour alone. PARTS: [CalendarDayButton](/components/calendar#calendar-day-button). - [Checkbox](/components/checkbox) (primitive, stable): Checkbox primitive for Orbit DS, built on Base UI. Import from `@orbit/ui/components/checkbox`. USE WHEN: Zero or more of several options can be true, or a value takes effect on save. NOT WHEN: Exactly one option must be chosen - that is RadioGroup. PREFERRED OVER: switch. RELATED: toggle. COMPOSED WITH: field, input, label. A11Y: Needs a real label or an aria-label; the box is not its name. indeterminate is a third state announced as "mixed", which an unchecked box would misreport. - [Combobox](/components/combobox) (primitive, beta): Combobox primitive for Orbit DS - a filtering select, built on Base UI Combobox. Import from `@orbit/ui/components/combobox`. USE WHEN: Picking from a long list where the reader searches rather than scans - and any multi-select, which Select does not do. NOT WHEN: The list is short and static - Select shows it without typing; two to four options are a RadioGroup. RELATED: select. COMPOSED WITH: avatar, field. A11Y: Full combobox semantics from Base UI: the input owns the listbox, arrow keys move the highlight, Escape closes and focus returns. Chip remove buttons are real buttons, reachable by keyboard. Needs a label; a placeholder is not a name. PARTS: [ComboboxValue](/components/combobox#combobox-value), [ComboboxTrigger](/components/combobox#combobox-trigger), [ComboboxInput](/components/combobox#combobox-input), [ComboboxContent](/components/combobox#combobox-content), [ComboboxList](/components/combobox#combobox-list), [ComboboxItem](/components/combobox#combobox-item), [ComboboxGroup](/components/combobox#combobox-group), [ComboboxLabel](/components/combobox#combobox-label), [ComboboxCollection](/components/combobox#combobox-collection), [ComboboxEmpty](/components/combobox#combobox-empty), [ComboboxSeparator](/components/combobox#combobox-separator), [ComboboxChips](/components/combobox#combobox-chips), [ComboboxChip](/components/combobox#combobox-chip), [ComboboxChipsInput](/components/combobox#combobox-chips-input). - [Date range picker](/components/patterns/date-range-picker) (pattern, beta): DateRangePicker is a labelled trigger opening a Popover with presets and a range Calendar. Import from `@orbit/patterns/patterns/date-range-picker`. USE WHEN: Filtering or reporting over a window of time - the FilterBar case. NOT WHEN: A single known date typed faster than picked - that is an Input with validation. A range the reader never changes is text, not a control. RELATED: calendar, filter-bar. A11Y: The trigger is a real button named by the label plus the chosen range, so the current value is announced. The calendar grid inherits react-day-picker's keyboard model; Escape and focus return come from the Popover. - [Field](/components/field) (primitive, stable): Field primitives for Orbit DS - the label, description and error scaffolding for any control. Import from `@orbit/ui/components/field`. USE WHEN: Any form control. It carries the label, the description and the error. NOT WHEN: The control is standalone and already labelled, like a toolbar search. AVOID: A placeholder is never a label. PREFERRED OVER: label. RELATED: form-section, label. COMPOSED WITH: avatar, button, checkbox, combobox, dialog, form, form-section, input, input-group, popover, radio-group, select, settings-section, stepper, switch, textarea. A11Y: Wire it yourself: aria-invalid on the control, aria-describedby pointing at the error, and matching id and htmlFor. A placeholder is not a label and an adornment icon is not a label. PARTS: [FieldSet](/components/field#field-set), [FieldLegend](/components/field#field-legend), [FieldGroup](/components/field#field-group), [FieldContent](/components/field#field-content), [FieldLabel](/components/field#field-label), [FieldTitle](/components/field#field-title), [FieldDescription](/components/field#field-description), [FieldSeparator](/components/field#field-separator), [FieldError](/components/field#field-error). - [Filter bar](/components/patterns/filter-bar) (pattern, stable): FilterBar is the single container for search, filters and table-level actions. Import from `@orbit/patterns/patterns/filter-bar`. USE WHEN: A collection has search, filters, or table-level actions. NOT WHEN: There is one control - place it directly. AVOID: Do not reorder by passing children directly. RELATED: data-grid-toolbar, date-range-picker, resource-table, search-input. COMPOSED WITH: button, search-input, select. A11Y: Every control inside needs its own label. A row of unlabelled selects is unusable by voice. Announce the result count after filtering, so a screen reader learns the list changed. - [Form](/components/form) (primitive, stable): Form primitive for Orbit DS, built on Base UI Form - consolidated errors, native submit. Import from `@orbit/ui/components/form`. USE WHEN: Fields are submitted together. NOT WHEN: Each control commits on its own - that is a settings page. COMPOSED WITH: button, field, input. A11Y: A real form element, so Enter submits and the browser's own semantics apply. Validate on submit. Blur validation scolds people for fields they have not finished. - [Form actions](/components/patterns/form-actions) (pattern, stable): FormActions is the submit/cancel footer for a form, with a pending state. Import from `@orbit/patterns/patterns/form-actions`. USE WHEN: A form submits as a whole. NOT WHEN: Each section commits on its own - that is SettingsSection. AVOID: Do NOT label the submit "Submit" or "OK". The button should name the action and its object, so the reader never has to scroll back up to learn what they are agreeing to. A11Y: The submit names what it does - "Create application", never "Submit". While pending both controls disable, so a slow save cannot be submitted twice. - [Form error summary](/components/patterns/form-error-summary) (pattern, stable): FormErrorSummary lists every failed field at the top of a form, each linking to its control. Import from `@orbit/patterns/patterns/form-error-summary`. USE WHEN: A form is long enough that a failure could be missed. NOT WHEN: The form has one or two fields - the field error is enough. AVOID: Render nothing when there are no errors — never an empty "no problems" box. A11Y: Move focus here on a failed submit, so the reader is told what went wrong rather than discovering it. Each entry links to its control by id and focuses it, rather than only scrolling to it. - [Form section](/components/patterns/form-section) (pattern, stable): FormSection groups related fields under a heading, so a long form stays navigable. Import from `@orbit/patterns/patterns/form-section`. USE WHEN: A long form needs grouping so it stays navigable. NOT WHEN: The form is short - grouping adds ceremony without help. RELATED: field. COMPOSED WITH: field, input. A11Y: The legend is a direct child of the fieldset. Wrapping it in a div silently strips the group's accessible name. - [Input](/components/input) (primitive, stable): Input primitive for Orbit DS. Import from `@orbit/ui/components/input`. USE WHEN: A single line of free text. NOT WHEN: The value is chosen from a known set - that is Select or RadioGroup. AVOID: Do NOT rely on a placeholder as the label - it disappears the moment someone types, and it fails contrast in most palettes. USE INSTEAD: search-input, input-group. RELATED: search-input. COMPOSED WITH: button, checkbox, dialog, field, form, form-section, label, popover, settings-section. A11Y: Always inside a Field with a real label bound by htmlFor. When invalid, set aria-invalid and point aria-describedby at the message - a red border says nothing to a screen reader. - [Input group](/components/input-group) (primitive, stable): Input group for Orbit DS - an input with leading or trailing adornments. Import from `@orbit/ui/components/input-group`. USE WHEN: A field needs an adornment: an icon, a unit, a copy control. NOT WHEN: The adornment is the only label - it is not one. PREFERRED OVER: input. COMPOSED WITH: button, field. A11Y: The adornment is decorative. The control still needs its own label or aria-label. The whole group takes one focus ring, so the field reads as a single control. PARTS: [InputGroupAddon](/components/input-group#input-group-addon), [InputGroupButton](/components/input-group#input-group-button), [InputGroupText](/components/input-group#input-group-text), [InputGroupInput](/components/input-group#input-group-input), [InputGroupTextarea](/components/input-group#input-group-textarea). - [Label](/components/label) (primitive, stable): Label primitive for Orbit DS. Import from `@orbit/ui/components/label`. USE WHEN: Naming a control outside a Field. NOT WHEN: You are inside a Field - use FieldLabel, which wires the ids. USE INSTEAD: field. RELATED: field. COMPOSED WITH: checkbox, input. A11Y: htmlFor must match the control's id. Clicking the label then focuses the control, which is the point. - [Radio group](/components/radio-group) (primitive, stable): Radio group primitives for Orbit DS, built on Base UI. Import from `@orbit/ui/components/radio-group`. USE WHEN: Exactly one of two to five visible options. NOT WHEN: There are more than about five - that is Select. PREFERRED OVER: select. COMPOSED WITH: field. A11Y: Arrow keys move between options and the group is a single tab stop, which is what separates a radio group from a row of checkboxes. The group needs its own label - the options alone rarely say what they choose between. PARTS: [RadioGroupItem](/components/radio-group#radio-group-item). - [Search input](/components/patterns/search-input) (pattern, stable): SearchInput is the standard collection search field, with a leading icon. Import from `@orbit/patterns/patterns/search-input`. USE WHEN: A collection is searched by free text. NOT WHEN: You are filtering by a known set of values - that is a Select in a FilterBar. AVOID: Do NOT use this as a general text input, and do not let it be the only way to narrow a collection - searching demands that the reader already knows what to type. Pair it with filters in a `FilterBar`. PREFERRED OVER: input. RELATED: filter-bar, input. COMPOSED WITH: button, filter-bar, select. A11Y: Requires a label. A magnifier icon is not a name. Announce the result count after filtering, or the list appears to change silently. - [Select](/components/select) (primitive, stable): Select primitive for Orbit DS, built on Base UI Select. Import from `@orbit/ui/components/select`. USE WHEN: One value from a list too long to show at once. NOT WHEN: There are two or three options - RadioGroup shows them all. USE INSTEAD: radio-group. RELATED: combobox. COMPOSED WITH: button, field, filter-bar, search-input, stepper. A11Y: Full listbox semantics from Base UI: type-ahead, arrow keys, Escape, and focus return to the trigger. Needs a label; the current value is not a name. PARTS: [SelectGroup](/components/select#select-group), [SelectValue](/components/select#select-value), [SelectTrigger](/components/select#select-trigger), [SelectContent](/components/select#select-content), [SelectLabel](/components/select#select-label), [SelectItem](/components/select#select-item), [SelectSeparator](/components/select#select-separator), [SelectScrollUpButton](/components/select#select-scroll-up-button), [SelectScrollDownButton](/components/select#select-scroll-down-button). - [Settings section](/components/patterns/settings-section) (pattern, stable): SettingsSection is a titled block of settings that saves independently of its neighbours. Import from `@orbit/patterns/patterns/settings-section`. USE WHEN: A settings page, where each group saves independently. NOT WHEN: Everything submits together - that is Form with FormActions. COMPOSED WITH: field, input. A11Y: Each section commits on its own, so a failed save cannot discard another section's edits. The title names the section for assistive technology, which is why the submit may simply say "Save". - [Switch](/components/switch) (primitive, stable): Switch primitive for Orbit DS - an immediate on/off setting. Import from `@orbit/ui/components/switch`. USE WHEN: A setting takes effect immediately. NOT WHEN: The value is saved with a form - that is Checkbox. AVOID: Do NOT use a switch for a destructive setting without a confirmation - an immediate control gives no chance to reconsider. USE INSTEAD: checkbox. COMPOSED WITH: field. A11Y: Renders role="switch" with aria-checked, which is announced as on/off rather than checked/unchecked. For a value that only takes effect on save, use a Checkbox - a switch implies immediate effect. - [Textarea](/components/textarea) (primitive, stable): Textarea primitive for Orbit DS. Import from `@orbit/ui/components/textarea`. USE WHEN: Free text runs to more than one line. NOT WHEN: The value is one short line - that is Input. COMPOSED WITH: field. A11Y: Always inside a Field with a real label. Size it with rows, so it grows with the density token. ## Data display - [Activity feed](/components/patterns/activity-feed) (pattern, stable): ActivityFeed is a reverse-chronological list of events with an icon and a timestamp. Import from `@orbit/patterns/patterns/activity-feed`. USE WHEN: A reverse-chronological record of what happened. NOT WHEN: The events are comparable records the reader will filter - that is ResourceTable. A11Y: A real list, so the number of events is announced. Each timestamp is a time element with a machine-readable datetime, not just formatted text. - [Avatar](/components/avatar) (primitive, stable): Avatar primitives for Orbit DS - an image with an initials fallback. Import from `@orbit/ui/components/avatar`. USE WHEN: A person or team needs identifying in a row, a header or a stack. NOT WHEN: You need a logo or an entity icon - that is not a person. COMPOSED WITH: board, card, combobox, field, status-badge. A11Y: The image is decorative: alt is empty and the name is supplied by the text beside it, so a screen reader hears it once. The fallback initials are readable text, not an image, and inherit the surrounding contrast. PARTS: [AvatarImage](/components/avatar#avatar-image), [AvatarFallback](/components/avatar#avatar-fallback), [AvatarBadge](/components/avatar#avatar-badge), [AvatarGroup](/components/avatar#avatar-group), [AvatarGroupCount](/components/avatar#avatar-group-count). - [Badge](/components/badge) (primitive, stable): Badge primitive for Orbit DS, including the tinted status tones used across admin data. Import from `@orbit/ui/components/badge`. USE WHEN: Classifying something: an environment, a tier, a type. NOT WHEN: The thing being shown is a state that changes - that is StatusBadge. RELATED: status-badge. COMPOSED WITH: button, card, cell, detail-drawer, gradient-orb, item, lunar-surface, property-list, status-badge. A11Y: The label is real text, so classification survives greyscale, colourblindness and a screen reader. The dot is aria-hidden - it marks in-flight state for the eye only. - [Board](/components/patterns/board) (pattern, beta): Board lays records out as stage columns - a kanban view of a lifecycle, read-only. Import from `@orbit/patterns/patterns/board`. USE WHEN: Stage-at-a-glance over a lifecycle - a triage pipeline, a delivery board. NOT WHEN: The reader works the records rather than surveying them - sorting, filtering and bulk actions are ResourceTable's job. AVOID: Do NOT reach for a board when the records need sorting, filtering or comparing across fields - that is ResourceTable. Keep to about six columns and a few dozen cards per column; past that the glance the board exists for is gone. RELATED: resource-table. COMPOSED WITH: avatar, card, status-badge. A11Y: Each column is a labelled region with a real list inside, so a screen reader gets the stage names and per-stage counts the layout shows. The count is text beside the label, never a colour signal. PARTS: [BoardColumn](/components/patterns/board#board-column), [BoardCard](/components/patterns/board#board-card). - [Cell](/components/cell) (primitive, stable): Table cell content helpers for Orbit DS - the vocabulary that makes dense rows readable. Import from `@orbit/ui/components/cell`. USE WHEN: Composing a table cell that needs two lines, an identifier, or aligned digits. COMPOSED WITH: badge, button, detail-drawer, property-list, resource-table, status-badge, table. A11Y: Layout helpers only. CellNumeric right-aligns and tabularises digits, which helps everyone scanning a column. PARTS: [CellStack](/components/cell#cell-stack), [CellMono](/components/cell#cell-mono), [CellNumeric](/components/cell#cell-numeric). - [Data grid](/components/data-grid) (primitive, experimental): A virtualized data grid for dense work surfaces, built on a real table element. Import from `@orbit/ui/components/data-grid`. USE WHEN: The reader works IN the data - editing, ranging, scanning tens of thousands of rows. NOT WHEN: The reader only reads the collection and acts on whole rows - ResourceTable keeps the browser's own table semantics and owes them no keyboard model. AVOID: Do NOT reach for it to display a collection a reader only scans and acts on by whole rows. Choosing `role="grid"` is choosing to own the keyboard: it tells a screen reader to leave browse mode, and its own table commands stop working. `ResourceTable` keeps the browser's native table semantics and owes the reader no keyboard model. RELATED: data-grid-column-panel, data-grid-export-menu, data-grid-filter-panel, data-grid-group-panel, data-grid-pivot-panel, data-grid-toolbar, resource-table, table. COMPOSED WITH: button, data-grid-column-panel, data-grid-export-menu, data-grid-filter-panel, data-grid-group-panel, data-grid-pivot-panel, data-grid-toolbar, popover, status-badge. A11Y: The grid is one tab stop - exactly one cell is tabindex=0 and the arrow keys move it, so 10,000 rows never become 40,000 tab stops. aria-rowcount and aria-colcount report the whole dataset, and every rendered row carries its absolute aria-rowindex, so virtualization never shrinks what a screen reader is told. The column marked rowHeader renders as role=rowheader, so a reader deep in the columns still hears which row they are on. Focus is tracked by row id, so it follows the reader's row through a sort; when a focused cell scrolls out of the DOM it parks on the grid rather than falling to the body. Sorting and selection are announced through a polite live region, because neither moves focus. A cell range covers DATA cells: a group row is navigable but never selectable, so what the announced count promises is what Delete, a copy and a fill actually act on. - [Data grid column panel](/components/patterns/data-grid-column-panel) (pattern, experimental): The panel where a reader chooses which data grid columns they want, and in what order. Import from `@orbit/patterns/patterns/data-grid-column-panel`. USE WHEN: A grid has enough columns that a reader needs to choose between them. NOT WHEN: The grid has six columns and nothing to configure - a panel trigger that opens a short list is chrome for its own sake. AVOID: Do NOT use it as the only way to reach a column's own actions - sorting and fitting belong in the column's menu, beside the column, where a reader is already looking. RELATED: data-grid. COMPOSED WITH: button, data-grid, popover. A11Y: Each column is a real checkbox with a real label, so the list is operable and readable without sight of the grid. Reordering is done with buttons, never drag alone - and each announces the column's new position through the caller's live region. The search field filters the list only; it never changes which columns are shown in the grid. - [Data grid export menu](/components/patterns/data-grid-export-menu) (pattern, experimental): The menu that takes a data grid's rows out: CSV, the clipboard, and the printer. Import from `@orbit/patterns/patterns/data-grid-export-menu`. USE WHEN: A grid's rows have to leave the screen - a spreadsheet, a paste, a printout. NOT WHEN: There is exactly one way out. A single named button beats a menu of one. AVOID: Do NOT offer an export of the whole dataset from a server-side grid. The grid holds one page; only the server can export the rest, and a button that silently exports 25 of 40,000 rows is worse than no button. RELATED: data-grid. COMPOSED WITH: button, data-grid, data-grid-toolbar. A11Y: A real menu from a named button, so the choices are reachable and announced rather than being a row of icons. Each entry names the rows it acts on, so what a reader is about to get is in the words rather than in the state of the grid behind the menu. - [Data grid filter panel](/components/patterns/data-grid-filter-panel) (pattern, experimental): The multi-filter builder for a data grid: a column, an operator, a value, per row. Import from `@orbit/patterns/patterns/data-grid-filter-panel`. USE WHEN: A reader needs to narrow a grid by more than one thing at once, or by a range. NOT WHEN: One search field would do - that is the toolbar's quick filter. AVOID: Do NOT reach for it where one search field would do. A three-control row per condition is a lot of interface, and `DataGridToolbar`'s quick filter answers "find the thing I am thinking of" better than a filter builder ever will. RELATED: data-grid, data-grid-toolbar. COMPOSED WITH: data-grid, data-grid-toolbar, status-badge. A11Y: Every control in a condition row is labelled with the column it belongs to, so a reader arriving on the third select is told what it selects. The and/or choice is one control for the whole set rather than one between each pair, because a mixture of the two is not a model this panel can express. Removing a condition names the condition it removes, never just "Remove". - [Data grid group panel](/components/patterns/data-grid-group-panel) (pattern, experimental): The panel where a reader decides how a data grid is grouped and what each group reports. Import from `@orbit/patterns/patterns/data-grid-group-panel`. USE WHEN: A grid holds enough rows that a reader needs them collapsed into groups to see the shape of the data. NOT WHEN: One grouping is the only one that ever makes sense - pass `groupBy` and leave the panel out. AVOID: Do NOT offer an aggregation on a column that names something. "Sum of Service" is not a quantity, and a panel that offers it invites a reader to produce nonsense. RELATED: data-grid, data-grid-pivot-panel. COMPOSED WITH: data-grid. A11Y: Every control is a real button or a real select with a real label, so the panel is operable and readable without sight of the grid. Each grouping level says its position in words - "1." before the column name - so the order survives being read aloud one line at a time. Expand all and collapse all are buttons, not a disclosure state, because they act on many rows rather than toggling one. - [Data grid pivot panel](/components/patterns/data-grid-pivot-panel) (pattern, experimental): The panel that builds a pivot: what goes down the side, what spreads across, what fills the cells. Import from `@orbit/patterns/patterns/data-grid-pivot-panel`. USE WHEN: A reader needs to compare one measure across two dimensions at once - replicas by service AND environment. NOT WHEN: One dimension is enough. Grouping answers that with the rows still intact, and a pivot throws the rows away. AVOID: Do NOT put a high-cardinality field across the top. One derived column per distinct value is the point of a pivot, and forty of them is not a table anybody can read - `pivot()` clips at forty and reports how many it dropped, which is a fallback, not a plan. RELATED: data-grid, data-grid-group-panel. COMPOSED WITH: data-grid. A11Y: Each axis is a labelled list, and every move is a named button - "Move Service across", not a drag with no keyboard equivalent. The measure and its aggregation are a real select with a real label, so what the cells report can be read aloud. - [Data grid toolbar](/components/patterns/data-grid-toolbar) (pattern, experimental): The bar above a data grid's rows: quick filter, how much survived it, and the grid's actions. Import from `@orbit/patterns/patterns/data-grid-toolbar`. USE WHEN: A grid needs a quick filter, a count, or any control that acts on the whole grid. NOT WHEN: The collection is not a DataGrid - FilterBar is the same bar for an ordinary table or list. AVOID: Do NOT give it `role="toolbar"`. That role obliges arrow-key navigation among its own controls, which fights the grid's arrows two pixels below it; named buttons are enough, and `FilterBar` sets no role either. RELATED: data-grid, data-grid-filter-panel, filter-bar. COMPOSED WITH: button, data-grid, data-grid-export-menu, data-grid-filter-panel, status-badge. A11Y: The count is a polite live region: filtering changes what is on screen without moving focus, so a reader who cannot see it would otherwise never be told. No role=toolbar, so the arrow keys keep belonging to the grid rather than to the bar above it. The selection bar names the count in words, and its clear control says what it clears. - [Property list](/components/patterns/property-list) (pattern, stable): PropertyList renders a label/value ladder, the standard way to show entity attributes. Import from `@orbit/patterns/patterns/property-list`. USE WHEN: Showing the attributes of one entity. NOT WHEN: You are showing many entities - that is a table. USE INSTEAD: resource-table. COMPOSED WITH: badge, button, cell, detail-drawer, status-badge. A11Y: A real dl, so each label is programmatically tied to its value rather than merely sitting beside it. - [Ranked list](/components/patterns/ranked-list) (pattern, stable): RankedList shows an ordered leaderboard with a value and a proportional bar. Import from `@orbit/patterns/patterns/ranked-list`. USE WHEN: An ordered leaderboard where proportion matters. NOT WHEN: Order is not meaningful - that is a plain list. RELATED: chart, chart-card, segmented-bar, utilisation-meter. A11Y: The rank and the value are text; the proportional bar is decoration and is aria-hidden. - [Resource table](/components/patterns/resource-table) (pattern, beta): ResourceTable is the standard Orbit collection view: toolbar, selection, rows, pagination. Import from `@orbit/patterns/patterns/resource-table`. USE WHEN: Users manage a collection: filtering, selecting, sorting, acting on rows. NOT WHEN: The data is analytical or read-only - a plain Table is lighter. AVOID: Do NOT use it for dashboards, comparison tables, or a handful of read-only key/value rows - a plain Table is lighter and clearer for those. PREFERRED OVER: property-list, table. RELATED: board, data-grid, filter-bar, pagination, table. COMPOSED WITH: button, cell, status-badge. A11Y: The label prop becomes the table's caption, so the collection is named. Sortable headers are real buttons inside th scope="col" carrying aria-sort. onRowClick is an enhancement, never the accessible path - rows are not focusable, so expose the same action in rowActions. - [Status badge](/components/patterns/status-badge) (pattern, stable): StatusBadge maps a domain's status vocabulary onto Orbit's semantic tones. Import from `@orbit/patterns/patterns/status-badge`. USE WHEN: Showing a state that changes: live, degraded, failed. NOT WHEN: The value merely classifies and never changes - that is Badge. AVOID: Do NOT use this for attributes that merely classify rather than indicate state (a region, a plan name, a fulfilment method) - use `Badge variant="outline"`. RELATED: badge. COMPOSED WITH: avatar, badge, board, button, card, cell, data-grid, data-grid-filter-panel, data-grid-toolbar, detail-drawer, property-list, resource-table. A11Y: The status label always ships as text, so state survives greyscale and a screen reader. The map is required because "info" means Deploying in one domain and Scheduled in another. - [Stepper](/components/patterns/stepper) (pattern, beta): Stepper shows where an item sits in an ordered lifecycle - done, current, upcoming. Import from `@orbit/patterns/patterns/stepper`. USE WHEN: Showing where a record sits in an ordered lifecycle - triage to approved, draft to published. NOT WHEN: The reader chooses between views rather than reading progression - that is Tabs or SegmentedControl. A form wizard needs navigation and validation this pattern deliberately does not do. AVOID: Do NOT use status hues for steps - lifecycle position is not success or danger, and a green "done" beside a real success badge would make them one signal. RELATED: segmented-control. COMPOSED WITH: field, select. A11Y: An ordered list with aria-current="step" on the current item; each step's state is visually hidden text, so position is never colour or a checkmark alone. Non-interactive: no step is focusable, so a long lifecycle adds no tab stops. PARTS: [StepperItem](/components/patterns/stepper#stepper-item). - [Table](/components/table) (primitive, stable): Semantic table primitives tuned for dense Orbit admin data. Import from `@orbit/ui/components/table`. USE WHEN: Rows are comparable and the reader scans down columns. NOT WHEN: The collection is managed - filtered, selected, paged - that is ResourceTable. USE INSTEAD: resource-table. RELATED: data-grid, resource-table. COMPOSED WITH: cell, chart, chart-card, segmented-control, table-sort-button. A11Y: Give the table a caption or an accessible name; "table with 12 rows" alone tells a reader nothing. A sortable header is a real button inside a th scope="col" carrying aria-sort. PARTS: [TableHeader](/components/table#table-header), [TableBody](/components/table#table-body), [TableFooter](/components/table#table-footer), [TableRow](/components/table#table-row), [TableHead](/components/table#table-head), [TableCell](/components/table#table-cell), [TableCaption](/components/table#table-caption). - [Table sort button](/components/patterns/table-sort-button) (pattern, stable): TableSortButton makes a column header operable by keyboard and announces sort state. Import from `@orbit/patterns/patterns/table-sort-button`. USE WHEN: A column header needs to be sortable. COMPOSED WITH: table. A11Y: A real button inside the header cell, so sorting is reachable by keyboard. The header carries aria-sort, so the current sort is announced rather than only drawn as an arrow. ## Metrics & charts - [Chart](/components/chart) (primitive, beta): Chart primitives for Orbit DS, wrapping recharts with tokenised grid, axis and tooltip. Import from `@orbit/ui/components/chart`. USE WHEN: You need a chart form ChartCard does not already frame. NOT WHEN: A plain number or a Sparkline would answer the question. AVOID: Do NOT mint extra hues for a breakdown with more than five categories - fold into "Other", use small multiples, or use RankedList, where each bar is labelled in text and the palette cap does not apply. RELATED: chart-card, ranked-list, sparkline. COMPOSED WITH: chart-card, segmented-control, table. A11Y: The plot is not readable by assistive technology. Every Orbit chart ships a table view alongside it - that is the accessible path. A legend is always present for two or more series, so identity is never colour alone. PARTS: [ChartStyle](/components/chart#chart-style), [ChartTooltip](/components/chart#chart-tooltip), [ChartTooltipContent](/components/chart#chart-tooltip-content), [ChartLegend](/components/chart#chart-legend), [ChartLegendContent](/components/chart#chart-legend-content). - [Chart card](/components/patterns/chart-card) (pattern, stable): ChartCard is the frame every Orbit chart lives in: title, caption, control, plot, table view. Import from `@orbit/patterns/patterns/chart-card`. USE WHEN: Any chart on a Orbit page - it supplies the title, the control and the table view. NOT WHEN: You are inside a metric tile - that is Sparkline. AVOID: Do NOT put two measures of different scale in one chart on a second axis. Use two charts, small multiples, or index both to a common base - a dual axis lets the reader infer a correlation that the data does not support. RELATED: chart, ranked-list. COMPOSED WITH: chart, segmented-control, table. A11Y: The table view is the accessible path to the data and is required, not optional. The title names what is measured, so a single-series chart needs no legend. - [Delta chip](/components/patterns/delta-chip) (pattern, stable): DeltaChip shows a change, with direction and meaning as SEPARATE props. Import from `@orbit/patterns/patterns/delta-chip`. USE WHEN: A number needs its change and the meaning of that change. NOT WHEN: There is nothing to compare against - omit it rather than showing zero. AVOID: Do NOT reach for `positive` or `negative` when a change is merely a change - headcount, or a version number. Overusing the status colours drains them of meaning, which is exactly what `neutral` protects against. RELATED: metric-card. COMPOSED WITH: metric-card, sparkline. A11Y: Direction and sentiment are separate, so the arrow and the colour can disagree - a falling error rate is good news. The number and its sign are text; colour is never the only signal. - [Metric card](/components/patterns/metric-card) (pattern, beta): MetricCard is the standard KPI tile - label, value, change, and an optional trend line. Import from `@orbit/patterns/patterns/metric-card`. USE WHEN: A single headline number with its context. NOT WHEN: You have two numbers - that is two cards. PREFERRED OVER: card. RELATED: delta-chip, sparkline. COMPOSED WITH: delta-chip, sparkline. A11Y: The label and value are text. The sparkline is aria-hidden, so the number always carries the magnitude. One metric per card; a tile with two numbers has no headline for a screen reader to announce. PARTS: [MetricGrid](/components/patterns/metric-card#metric-grid). - [Progress](/components/progress) (primitive, stable): Progress primitives for Orbit DS - a pill track with a filled indicator. Import from `@orbit/ui/components/progress`. USE WHEN: Work is running and its completion is known. NOT WHEN: You are showing consumption against a limit - that is UtilisationMeter. AVOID: Do NOT use Progress for an indeterminate wait - use a spinner or a skeleton, which do not imply a knowable end. USE INSTEAD: utilisation-meter. RELATED: skeleton. A11Y: Renders role="progressbar" with its current and maximum values, so progress is announced rather than only drawn. PARTS: [ProgressTrack](/components/progress#progress-track), [ProgressIndicator](/components/progress#progress-indicator), [ProgressLabel](/components/progress#progress-label), [ProgressValue](/components/progress#progress-value). - [Segmented bar](/components/patterns/segmented-bar) (pattern, beta): SegmentedBar shows how one whole divides - a proportional bar split into labelled segments. Import from `@orbit/patterns/patterns/segmented-bar`. USE WHEN: One total's composition matters - counts by status, capacity by class, work by owner. NOT WHEN: The composition changes over time and the change is the point - that is a stacked BarChart in a ChartCard. A single value against a ceiling is UtilisationMeter. RELATED: ranked-list, utilisation-meter. A11Y: The bar is aria-hidden decoration; the legend is a real list carrying each segment's label and value as text. PARTS: [SegmentedBarList](/components/patterns/segmented-bar#segmented-bar-list). - [Sparkline](/components/sparkline) (primitive, stable): Sparkline is a tiny inline-SVG trend line for metric tiles. Import from `@orbit/ui/components/sparkline`. USE WHEN: A metric tile needs shape alongside its number. NOT WHEN: The values themselves matter - that is a chart with axes. AVOID: Never render a sparkline as the only representation of data - it is `aria-hidden`, so it must always sit beside a real value. RELATED: chart, metric-card. COMPOSED WITH: delta-chip, metric-card. A11Y: aria-hidden. It shows shape, not values, so it must sit beside a real number that carries the magnitude. - [Utilisation meter](/components/patterns/utilisation-meter) (pattern, stable): UtilisationMeter shows consumption against a limit - quota, capacity, budget. Import from `@orbit/patterns/patterns/utilisation-meter`. USE WHEN: Consumption against a limit, where approaching the limit matters. NOT WHEN: The work simply completes - that is Progress. PREFERRED OVER: progress. RELATED: ranked-list, segmented-bar. A11Y: The numbers ship alongside the bar: a bar communicates proportion but never a precise value. Approaching a limit is signalled by the caption as well as the colour. ## Overlays - [Confirm dialog](/components/patterns/confirm-dialog) (pattern, stable): ConfirmDialog gates a consequential action behind an explicit, named confirmation. Import from `@orbit/patterns/patterns/confirm-dialog`. USE WHEN: An action is hard to undo and needs an explicit decision. NOT WHEN: The action is routine and reversible - just do it, and offer undo. AVOID: Do NOT use this for routine, easily reversible actions - a toast with an Undo action is better there, because it does not interrupt. PREFERRED OVER: dialog. RELATED: dialog. COMPOSED WITH: button. A11Y: The confirm label names the action and its object - "Archive Checkout API", never "Confirm" - because it is the last thing read before committing. Escape and outside click both cancel. A destructive action is never the default focus. - [Detail drawer](/components/patterns/detail-drawer) (pattern, stable): DetailDrawer is the side panel for inspecting one entity without leaving the list. Import from `@orbit/patterns/patterns/detail-drawer`. USE WHEN: Inspecting one record without losing the list. NOT WHEN: The record needs a page of its own - link to it. AVOID: Do not use it for multi-step flows or long forms: those deserve a page, because a drawer cannot be linked to, and its width fights dense forms. Never open a drawer from inside another drawer. PREFERRED OVER: sheet. RELATED: sheet. COMPOSED WITH: badge, button, cell, property-list, status-badge. A11Y: Traps focus and returns it to the row that opened it. Rows are not focusable by design; whatever the drawer does must also be reachable from rowActions. PARTS: [DetailDrawerHeader](/components/patterns/detail-drawer#detail-drawer-header), [DetailDrawerBody](/components/patterns/detail-drawer#detail-drawer-body), [DetailSection](/components/patterns/detail-drawer#detail-section), [DetailDrawerFooter](/components/patterns/detail-drawer#detail-drawer-footer). - [Dialog](/components/dialog) (primitive, stable): Dialog is the centered modal overlay primitive, built on Base UI Dialog. Import from `@orbit/ui/components/dialog`. USE WHEN: A decision or a short task needs the reader's full attention. NOT WHEN: It is a yes/no confirmation - ConfirmDialog names the action for you. AVOID: Do NOT use a Dialog for browsing or for long forms - it cannot be linked to, and it traps the reader in a task they cannot leave half-finished. USE INSTEAD: confirm-dialog. RELATED: confirm-dialog, popover. COMPOSED WITH: button, field, input. A11Y: Base UI supplies the focus trap, focus return to the trigger, and Escape. Never reimplement any of the three. Needs a DialogTitle - it becomes the dialog's accessible name. PARTS: [DialogTrigger](/components/dialog#dialog-trigger), [DialogPortal](/components/dialog#dialog-portal), [DialogClose](/components/dialog#dialog-close), [DialogOverlay](/components/dialog#dialog-overlay), [DialogContent](/components/dialog#dialog-content), [DialogHeader](/components/dialog#dialog-header), [DialogFooter](/components/dialog#dialog-footer), [DialogTitle](/components/dialog#dialog-title), [DialogDescription](/components/dialog#dialog-description). - [Popover](/components/popover) (primitive, beta): Popover primitive for Orbit DS - a small anchored panel, built on Base UI Popover. Import from `@orbit/ui/components/popover`. USE WHEN: Anchored content with interaction - pickers, small forms, rich explanations. NOT WHEN: One line of text names or explains the control - that is Tooltip. A decision that blocks the flow is a Dialog; a workflow's details are a DetailDrawer. RELATED: dialog, tooltip. COMPOSED WITH: button, data-grid, data-grid-column-panel, field, input. A11Y: Focus moves into the panel on open and returns to the trigger on close; Escape dismisses. All from Base UI - never reimplement them. The trigger needs an accessible name; give the panel one with PopoverTitle when its purpose is not obvious from the trigger. PARTS: [PopoverTrigger](/components/popover#popover-trigger), [PopoverContent](/components/popover#popover-content), [PopoverHeader](/components/popover#popover-header), [PopoverTitle](/components/popover#popover-title), [PopoverDescription](/components/popover#popover-description). - [Tooltip](/components/tooltip) (primitive, stable): Tooltip primitives for Orbit DS, on the shared inverse surface. Import from `@orbit/ui/components/tooltip`. USE WHEN: A labelled control needs a reason, a shortcut, or a constraint explained. NOT WHEN: The content is essential - a tooltip is not readable on touch. AVOID: Do NOT put an action, a link or anything the reader must interact with inside one. RELATED: popover. COMPOSED WITH: button. A11Y: A tooltip supplements a name, it never supplies one. The trigger must be labelled without it. Shown on focus as well as hover, and dismissed by Escape. Deliberately inert on touch devices - never make a tooltip the only copy of anything. PARTS: [TooltipProvider](/components/tooltip#tooltip-provider), [TooltipTrigger](/components/tooltip#tooltip-trigger), [TooltipContent](/components/tooltip#tooltip-content). ## Feedback - [Alert](/components/alert) (primitive, stable): Alert is the inline banner for page- or section-level messages. Import from `@orbit/ui/components/alert`. USE WHEN: A condition applies to the whole page or a whole section. NOT WHEN: The message concerns one field - that is FieldError. AVOID: Do NOT make an alert dismissible when it describes a blocking condition - the reader can hide a problem they cannot yet fix. USE INSTEAD: inline-alert. COMPOSED WITH: button. A11Y: Renders role="alert", so it is announced when it appears. Do not mount one on page load - nothing has changed yet. The tone is carried by an icon and the text, never by colour alone. PARTS: [AlertTitle](/components/alert#alert-title), [AlertDescription](/components/alert#alert-description), [AlertAction](/components/alert#alert-action). - [Empty](/components/empty) (primitive, stable): Empty-state primitives for Orbit DS - icon, title, description and an optional action. Import from `@orbit/ui/components/empty`. USE WHEN: A view has no content and the reader needs to know why. NOT WHEN: The view is loading - that is Skeleton. RELATED: skeleton. COMPOSED WITH: button. A11Y: A heading and body text, not an image. The reason the view is empty is readable, not implied by an illustration. PARTS: [EmptyHeader](/components/empty#empty-header), [EmptyMedia](/components/empty#empty-media), [EmptyTitle](/components/empty#empty-title), [EmptyDescription](/components/empty#empty-description), [EmptyContent](/components/empty#empty-content). - [Inline alert](/components/patterns/inline-alert) (pattern, stable): InlineAlert is the persistent, in-page banner for section- or page-level messages. Import from `@orbit/patterns/patterns/inline-alert`. USE WHEN: A condition must persist until it is resolved. NOT WHEN: The message is a result that can be missed - that is a toast. PREFERRED OVER: alert. COMPOSED WITH: button. A11Y: The tone is carried by an icon and the text as well as the colour. Not dismissible when it describes a blocking condition - the reader can hide a problem they cannot yet fix. - [Skeleton](/components/skeleton) (primitive, stable): Skeleton primitive for Orbit DS - a placeholder block while content loads. Import from `@orbit/ui/components/skeleton`. USE WHEN: Content is loading and its shape is known. NOT WHEN: The shape is unknown - use a spinner. AVOID: Do NOT skeleton an entire page - hold the layout and skeleton only the parts that are actually pending. RELATED: empty, progress. A11Y: aria-hidden: it is a placeholder, not content. Announce loading separately with a live region. The pulse respects prefers-reduced-motion globally. - [Sonner](/components/sonner) (primitive, stable): The Orbit toast host. Mount exactly once per app, near the root. Import from `@orbit/ui/components/sonner`. USE WHEN: A result can be missed and does not need acknowledging. NOT WHEN: The message must persist - that is InlineAlert. A11Y: Toasts are announced politely and are reachable by keyboard. Never put the only copy of an error in a toast - it disappears. ## Brand - [Gradient orb](/components/gradient-orb) (primitive, experimental): A decorative gradient orb that floats behind content on brand surfaces. Import from `@orbit/ui/components/gradient-orb`. USE WHEN: A marketing or onboarding surface wants the brand's warmth behind the content. NOT WHEN: The surface is admin chrome or data-dense work UI - orbs are expressive, not ambient. AVOID: Do NOT use an orb as a container: nothing may be positioned inside one, and content that depends on an orb for contrast has no contrast. RELATED: lunar-surface. COMPOSED WITH: badge, button, card, lunar-surface. A11Y: Purely decorative: aria-hidden, pointer-events-none, and never carries content or meaning. It must not be the only thing separating text from a busy ground - that is a scrim's job. - [Lunar surface](/components/patterns/lunar-surface) (pattern, experimental): LunarSurface is the brand's dark ground - lunar gradient, optional texture and scrim. Import from `@orbit/patterns/patterns/lunar-surface`. USE WHEN: A hero, onboarding moment or premium band should read unmistakably as Orbit. NOT WHEN: The surface is everyday admin chrome - the work area stays light, with the rail carrying the brand. AVOID: Do NOT build working screens on it: admin UI belongs on the light work area, and a resource table on the lunar is a slide, not software. Glass cards, stellar CTAs and orbs are the sanctioned furniture here. RELATED: card, gradient-orb, hero, marketing-section. COMPOSED WITH: badge, button, card, gradient-orb, logo-row. A11Y: The surface forces dark tokens, so text meets dark-theme contrast automatically. When a texture image is set, pass a scrim wherever text sits over it - the plates are busy and an orb is never sufficient protection. The texture and scrim layers are aria-hidden decoration; content order is unaffected. ## Marketing - [Feature grid](/components/patterns/feature-grid) (pattern, experimental): FeatureGrid lays product capabilities out as a responsive list-semantics grid. Import from `@orbit/patterns/patterns/feature-grid`. USE WHEN: A marketing band enumerates what the product does, three to nine items. NOT WHEN: Items are interactive or link somewhere - use Cards, which read as discrete objects. RELATED: marketing-section. COMPOSED WITH: marketing-section. A11Y: Renders a ul/li list, so item count and position are announced. PARTS: [Feature](/components/patterns/feature-grid#feature). - [Hero](/components/patterns/hero) (pattern, experimental): Hero is the opening lunar band of a marketing page - display-scale title over the brand's dark ground. Import from `@orbit/patterns/patterns/hero`. USE WHEN: A marketing or landing page opens - the first screenful that names the product and carries its primary CTA. NOT WHEN: The band sits mid-page - use Section with a SectionHeader; a second hero demotes the first. AVOID: Do NOT put more than one stellar button in `actions` - the hero is one screenful, and the cap on marketing surfaces is one stellar per screenful. RELATED: lunar-surface, marketing-section. COMPOSED WITH: button. A11Y: The title renders the page's h1 - a marketing page's h1 lives in its hero. Pass as="h2" only when something above it already carries the h1. When texture is set, the bottom scrim is forced on, keeping text contrast over the plate. Orbs are aria-hidden decoration and never the contrast device. - [Logo row](/components/patterns/logo-row) (pattern, experimental): LogoRow is the "trusted by" strip - partner marks at uniform height with an optional label. Import from `@orbit/patterns/patterns/logo-row`. USE WHEN: A marketing page cites the organisations already using the product. NOT WHEN: There are fewer than three marks - two logos read as an exhaustive list, not momentum. AVOID: Do NOT recolour or crop a partner's mark - partner marks keep their own identity at equal optical weight. RELATED: marketing-section. COMPOSED WITH: lunar-surface. A11Y: Each mark's alt names the organisation; the list semantics announce how many there are. - [Marketing footer](/components/patterns/marketing-footer) (pattern, experimental): MarketingFooter is the site footer of a public marketing page - link columns, lockup, legal line. Import from `@orbit/patterns/patterns/marketing-footer`. USE WHEN: A public marketing page ends. NOT WHEN: The app is admin software - admin screens end where the content ends; they have no site footer. AVOID: Do NOT put a CTA in the footer - persistent chrome never carries stellar, and a footer that sells competes with the CTA band that should sit just above it. RELATED: marketing-nav. A11Y: Renders a footer landmark; each column is a nav named by its title, so link groups are announced and skippable. PARTS: [MarketingFooterColumn](/components/patterns/marketing-footer#marketing-footer-column). - [Marketing nav](/components/patterns/marketing-nav) (pattern, experimental): MarketingNav is the sticky top navigation of a public marketing site. Import from `@orbit/patterns/patterns/marketing-nav`. USE WHEN: A public marketing or landing page needs its top navigation. NOT WHEN: The app is admin software - use AppShell's AppTopbar, which is density-aware and rail-anchored. AVOID: Do NOT put a stellar button in the nav: persistent chrome repeats on every screenful, and the stellar cap is one per screenful - a stellar that is always on screen spends the brand's loudest colour on furniture. RELATED: marketing-footer. COMPOSED WITH: button. A11Y: Renders a nav landmark named "Main". The mobile disclosure is a real button with aria-expanded and aria-controls, and the folded links stay in the same nav landmark. Link children should be MarketingNavLink so the current page carries aria-current. PARTS: [MarketingNavLink](/components/patterns/marketing-nav#marketing-nav-link). - [Marketing section](/components/patterns/marketing-section) (pattern, experimental): Section is the full-bleed band a marketing page is built from, with a measured inner container. Import from `@orbit/patterns/patterns/marketing-section`. USE WHEN: A public marketing page needs full-bleed bands with a consistent measured content column and vertical rhythm. NOT WHEN: The page is admin software - use AppPage, which is density-aware and full-bleed by design. AVOID: Do NOT use Section inside an admin app - admin pages keep `AppPage` and its density-scaled `gap-stack` rhythm; `py-section` is marketing rhythm. RELATED: feature-grid, hero, logo-row, lunar-surface, pricing-tier, testimonial. COMPOSED WITH: feature-grid. A11Y: Renders a real section element. Give it an id when the nav links to it, so the anchor lands on the landmark. PARTS: [SectionHeader](/components/patterns/marketing-section#section-header). - [Pricing tier](/components/patterns/pricing-tier) (pattern, experimental): PricingTier is one plan in a pricing band - name, price, feature list and its action. Import from `@orbit/patterns/patterns/pricing-tier`. USE WHEN: A marketing pricing band lays out two to four plans. NOT WHEN: Plans differ across many dimensions - use a comparison Table, where rows can be scanned. AVOID: Do NOT highlight more than one tier - a recommendation that points twice points nowhere. RELATED: marketing-section. COMPOSED WITH: button. A11Y: The feature list is a real ul with a check icon that is aria-hidden - the text carries the meaning. The plan name is an h3, so tiers are navigable landmarks within the band's h2. - [Testimonial](/components/patterns/testimonial) (pattern, experimental): Testimonial renders a customer quote with real figure/blockquote/figcaption semantics. Import from `@orbit/patterns/patterns/testimonial`. USE WHEN: A marketing band quotes a customer or user by name. NOT WHEN: The quote is anonymous filler - an unattributed quote persuades nobody; cut the band instead. RELATED: marketing-section. A11Y: Renders figure/blockquote/figcaption, binding the quote to its attribution. The avatar is decorative - attribution is the visible text, so the image contributes no information and carries an empty alt. ## System - [Density provider](/components/density-provider) (primitive, stable): DensityProvider and useDensity - the runtime density axis for every Orbit app. Import from `@orbit/ui/components/density-provider`. USE WHEN: Mounting a Orbit app - it belongs once, above the shell. A11Y: Density changes size, never information. Font hierarchy, icon meaning and reading order are unaffected. - [Theme provider](/components/theme-provider) (primitive, stable): ThemeProvider and useTheme - light/dark/system theme for every Orbit app. Import from `@orbit/ui/components/theme-provider`. USE WHEN: Mounting a Orbit app - it belongs once, above the shell. AVOID: Do NOT add or remove the `dark` class yourself - that couples the app to the token implementation and will fight the provider on the next render. RELATED: theme-toggle. A11Y: Both themes are held to the same contrast floor; dark is not an automatic inversion. Never write the dark class yourself - the provider owns it and will fight you on the next render. - [Theme toggle](/components/patterns/theme-toggle) (pattern, stable): ThemeToggle is the topbar control that flips the app between light and dark. Import from `@orbit/patterns/patterns/theme-toggle`. USE WHEN: Every Orbit app's topbar - the reader chooses the theme, not the OS alone. RELATED: theme-provider. A11Y: An icon-only button with a real accessible name that says what a click does. The sun/moon icon is aria-hidden decoration; the name carries the meaning.