Skip to content

Case study / UXPective design system

Orbit Design System

Orbit is the design system behind UXPective: the foundations, components and rules every UXPective screen is built from. It is built to WCAG 2.1 AA for text, keyboard, focus and screen readers, with a lighter brand palette where the look calls for it.

Role
Designer and maintainer of Orbit, founder of UXPective
Surfaces
UXPective website, cohort landing pages and the cohort application form
Scope
11 foundations, 22 components, 182 tokens and a contribution model
Source
One token file feeding the Figma library and the coded components
Accessibility
WCAG 2.1 AA for text, keyboard, focus and screen readers, with 6 documented color exceptions
What Orbit changed
MeasureBefore OrbitWith Orbit
Blues shipping across screens31
Text field versions in use31
Days to compose a new screen from existing parts51
Components with an accessibility checklistNone22 of 22
Contrast pairs at WCAG 2.1 AA (the rest documented as exceptions)Unchecked28 of 34
Sections

Part 1 of 5

Why Orbit exists

The gradient problem

On one screen the brand gradient ran blue to purple to orange. On the next it ran orange to purple to blue. Same three colors, reversed, three weeks apart, both designed by me.

That was the symptom that stopped feeling like a one-off. Once I started looking, it kept happening. Three near-identical blues were drifting across screens. Form fields were rebuilt every time a new screen needed one, with different padding, different focus rings and three different ideas of what “required” looked like. The logo’s three colors were being pulled apart and used as flat palettes, as if the gradient between them had never existed.

Screen 01, the intended run: blue, purple, orange at 90°.
Screen 02, reversed: the same stops, mirrored.
Screen 03, re-angled: correct stops at 135°. Wrong, subtly.

Evidence: the title gradient, three runs, three weeks apart, all designed by me.

#00A7FF
Intended
#1DB2FF
Drifted
#3A9CD5
Drifted

Three blues, drifting (illustrative values). None is wrong on its own. Side by side, they read as a brand losing its grip.

The fracture wasn’t in any single decision. It was in re-deciding the same thing every time I opened the file. A trustworthy brand is one where the small decisions have been made once. Orbit is where those decisions live, so they aren’t made again on every screen.

When to systematize

Most small projects don’t earn one. A design system has to be built, maintained and documented, and that only pays back once the cost of not having one crosses a threshold. Three signals made the threshold clear.

Surfaces I maintained alone

Up to three or four, I could hold the whole UI in my head. Past that, every new screen started from a screenshot of the last one and inherited every uncorrected error in it.

How fast I shipped screens

Roughly one a week. At that pace, ad hoc consistency is a fiction: either there is a source to refer to, or there are forty slightly different button paddings.

The cost of re-deciding

Half of every new screen was rebuilding the obvious: fields, button states, section padding. A new screen took five days, and about four of them went to things that already existed.

Threshold. It stopped being optional the moment I rebuilt a text field for the third time and couldn’t remember which of the previous two was the right one. If the honest answer to “which version do I copy?” is “I don’t know”, the system is already necessary.

Scope of the library

Orbit holds foundations (the decisions every screen depends on) and components (patterns that recur). Everything else stays local to its screen.

In the system

  • 11 foundations, starting with tokens and accessibility
  • 22 components, each documented the same way and each with an accessibility checklist
  • Gradient tokens: the brand mark belongs here, not in pages
  • Form patterns: labels, validation, error summary
  • Interface writing rules
  • A contribution process and a component lifecycle

Out of the system

  • Anything that has shipped exactly once
  • Page-specific illustrations and hero treatments
  • Long-form editorial layouts
  • Marketing and editorial copy (Writing covers interface text)
  • Any pattern that hasn’t yet hurt to rebuild

Why 22 components. Each one either met the entry criteria, or exists because another component is built on it: Radio group sits under Segmented control, and Menu holds the data table’s row actions. Patterns that don’t meet the criteria yet are tracked in the component backlog.

Architecture

One token source feeds two consumers, and names match across all three. If the code button has variant="primary", the Figma component’s variant is Primary. A Figma-only variant for a single mock is the first step of drift, so new variants follow the same contribution process in both places.

Orbit’s layers
LayerLives inWho changes it
Token sourceOne JSON file, primitives and semanticsOwner
Design libraryFigma variables, text styles and componentsOwner and contributors, against the acceptance criteria
Code libraryCSS custom properties and componentsOwner and contributors, against the acceptance criteria
SurfacesUXPective website, cohort landing pages, cohort application formAnyone building a UXPective screen
DocumentationThis case study and the internal docs page, in the same orderWhoever ships the change

Part 2 of 5

Eleven foundations

Tokens, accessibility, color, typography, spacing, layout and direction, shape, elevation and layers, motion, icons and writing. Every component is composed from these, so a change here reaches every component.

Foundation 1 of 11

Tokens

Every visual decision in Orbit is a token, and every token sits in one of three tiers. Components and screens only ever reach the middle one.

Token tiers
TierWhat it holdsExampleWho uses it
PrimitiveRaw values with no opinionuxp-blue-700, uxp-space-4Semantic tokens only
SemanticA value with a jobuxp-color-text-link, uxp-color-border-inputComponents and screens
ComponentA component diverging from its semanticsNone yetAdded only when a second theme needs it

Naming convention

Every token reads uxp-{category}-{role}-{variant}-{state}, dropping parts that don’t apply. Scales that are steps use numbers (space, elevation, color stops). Sets of discrete choices use sizes (radius, motion durations, breakpoints, UI text). Every decision has exactly one name; the only aliases are the section spacing tokens, and they say so.

How names are built
CategoryPatternExample
Color, semanticuxp-color-{role}-{variant}-{state}uxp-color-text-link-hover
Color, statusuxp-color-status-{status}-{use}uxp-color-status-error-fg
Gradientuxp-gradient-{job}uxp-gradient-action
Text styleuxp-text-{role}-{size}uxp-text-ui-button-md
Spacinguxp-space-{step}uxp-space-6
Shapeuxp-radius-{size}, uxp-border-{px}uxp-radius-sm
Elevation and layersuxp-elevation-{step}, uxp-layer-{surface}uxp-layer-modal
Motionuxp-motion-{property}-{size}uxp-motion-duration-md
Accessibilityuxp-focus-{part}, uxp-target-{size}uxp-focus-halo

Token inventory

Every token in Orbit, counted from the stylesheet when this page was built
CategoryTokensIncludes
Color primitives55Brand ramps, secondaries, neutrals, alpha neutrals, status ramps
Color semantics48Text, surfaces, borders, actions, controls, status
Gradients3Brand, text, action
Accessibility7Focus ring and halo, target sizes
Spacing14Eleven steps, three section tokens
Shape7Five radii, three border widths
Elevation and layers12Five elevations, seven layers
Motion7Four durations, three easings
Type families3Display, body, mono
Masks2Scroll-edge fade, select chevron
Text styles19Editorial and interface sets
Breakpoints5xs to xl
Total182

Linting

The token lint
RuleFails on
Components consume semanticsAny primitive token in component code, including hover, active and dark-surface states
No raw colorsA hex, rgb or named color anywhere in component code
Semantics reference primitivesA semantic token whose value is anything other than a primitive
Spacing stays on the scaleA padding, margin or gap that isn’t built from space, border or focus tokens
Names existA reference to a token that isn’t defined

Fixed dimensions (control heights, icon and checkbox boxes, maximum widths) are part of each component’s spec rather than the spacing scale. In Figma, detached styles are reported per file. When this page is built, its own stylesheet and every specimen go through the same lint, and passes: 298 component rules, no primitives, no raw colors, no off-scale spacing.

Foundation 2 of 11

Accessibility

Orbit is built to WCAG 2.1 AA for text, keyboard operation, focus, names and announcements. A small set of brand colors (the interface blue, field edges and placeholders) is deliberately lighter than AA for a cleaner look; each is listed below as a documented exception. Where AAA is easy (44px targets, reduced motion), Orbit takes it by default.

Accessibility tokens and system rules
RuleToken or behaviorCriterion
Body text, labels, helper text and errors at least 4.5:1Text tokens resolve to ink-500 or darker, or to status 700 stops1.4.3
Large text at least 3:1 (24px, or 18.66px bold)Display text uses 600 stops or darker; uxp-gradient-text is built from them1.4.3
Focus and checked states at least 3:1uxp-focus-ring-color and checked fills use blue-6001.4.11
Focus always visible2px outline at 2px offset on :focus-visible; fields turn their edge into a 2px blue line with a soft uxp-focus-halo; a solid or transparent outline is always present, so forced-colors mode keeps focus visible2.4.7
Targetsuxp-target-md 44px default (meets AAA 2.5.5); uxp-target-sm 32px for desktop dense areas onlybeyond AA
Text resiliencemin-height, never height; rem units; content survives 200% zoom, 320px width and text-spacing overrides1.4.4, 1.4.10, 1.4.12
One announcer per pageA polite live region owned by the system; components call it instead of building their own4.1.3
MotionDurations collapse under prefers-reduced-motion; only short opacity changes remainbeyond AA (2.3.3)
Visible edges on fields and small controlsFields and checkboxes keep a light edge (a documented exception to 3:1); buttons and chips are identified by their text1.4.11
Color is never the only cueUnderlined links, icon shapes per status, weight on selected tabs, a bar on the current nav item, thumb position on switches1.4.1
Single-key shortcutsNot used. Shortcuts include a modifier (Ctrl K, Cmd K)2.1.4

Component checklist

Every component page in Part 4 ends with the same seven rows, in the same order: contrast; keyboard; focus; name, role and value; announcements; reflow and text spacing; and one criterion specific to that component. A row is never deleted for being inconvenient. If it doesn’t apply, it says “Not applicable” and why.

Testing

A spec can be correct and still ship broken, so every component is checked on the built version with the same protocol: an automated scan, a keyboard-only pass, NVDA with Firefox and Chrome, VoiceOver with Safari on macOS and iOS, 200% zoom, a 320px viewport, a text-spacing override, and Windows forced-colors mode. Automated scans catch roughly a third of issues; the rest need a person.

Example. While these docs were built, the automated pass flagged links sitting on 50 tints, inside banners and selected table rows. A blue that works on white loses contrast on every tint, so links on tints use uxp-color-text-link-on-tint (blue-700), at 4.9:1 or better on every tint.

Documented exceptions

These pairs sit below their WCAG 2.1 AA requirement on purpose. The build fails if any other pair falls short, so the list below is always complete.

Documented exceptions to WCAG 2.1 AA contrast
PairRatioRequirementWhy it’s lighter
ink-400 on white3.2:14.5:1Placeholders are lighter so they never read as typed values. They’re examples only: every field also has a visible label.
blue-600 on white4.0:14.5:1A brighter interface blue keeps actions light. Links are also underlined, so they never rely on color.
blue-600 on ink-503.7:14.5:1The same interface blue on muted footers and button bars, where it sits slightly lower than on white.
white on blue-6004.0:14.5:1Same blue, carrying white labels. Labels are 15px semibold, and the pair still meets the 3:1 AA level for large text.
ink-200 on white1.4:13.0:1Fields keep a near-white edge for a lighter form. Each field has a visible label above it, and focus turns the edge blue.
ink-300 on white1.7:13.0:1Small controls get a slightly stronger edge than fields. Checked and on states switch to a blue fill that meets 3:1.

Foundation 3 of 11

Color

Three primaries from the brand mark, three decorative secondaries, eight cool neutrals, four status families and three gradients. The palette expresses personality; the status layer communicates state; they stay separate on purpose. Every stop has a job, which is what makes contrast predictable.

Stop usage
StopUsed forNotes
500Brand mark, decorative fills, illustrationThe brand colors. Logotypes are exempt from contrast, so the mark keeps them exactly.
600Interface actions and display text: buttons, links, focus, checked and selected statesTuned bright rather than dark. White on blue-600 is 4.0:1, a documented exception for text.
700Hover and pressed states, text on tints5.5:1 on white and at least 4.9:1 on every tint.
800 to 900Deep accents, used sparinglySaturated rather than darkened, so they never read as muddy.
50 to 200Tints, hover fills, selected rowsBackgrounds only, never text.

Primary colors

uxp-blue 50 to 900

uxp-purple 50 to 900

uxp-orange 50 to 900

500 is the brand color in each ramp. 600 carries the interface; the darker stops stay saturated instead of turning brown or navy.

Secondary colors

coral-500 #FF9884
pink-500 #F882E0
lpurple-500 #AA75FF

One secondary per composition, never as text, never as state, never next to purple-500.

The last rule exists because light purple and pink sit close to purple-500; used together they would read as three near-identical purples.

Neutrals

ink-0 #FFFFFF
Base surface
ink-50 #F7F7F9
Muted surfaces, footers, disabled fills
ink-100 #EEEEF1
Hairlines, chips, subtle fills
ink-200 #D7D7DC
Dividers on muted surfaces
ink-400 #8E8E93
Edges of fields and controls
ink-500 #6E6E73
Secondary text, placeholder, switch thumb
ink-700 #48484D
Tooltips, neutral badge text
ink-900 #1F1F22
Primary text

Soft cool neutrals, from a near-white muted surface to near-black text. Fields use ink-200 for their edge and small controls use ink-300, both lighter than 3:1 by choice.

One alpha primitive completes the neutrals: uxp-ink-900-a40, for the modal backdrop. It is the only translucent value in the system, and the backdrop’s semantic token points at it like any other primitive, so no semantic token ever holds a raw value.

Status colors

Status colors are kept separate from the brand palette, so a warning always looks like a warning rather than a brand moment. Warning is yellow rather than amber: amber sits 6° from brand orange, is lighter than it, and the two measure 1.1:1 against each other. Yellow sits 17° away, and every warning also carries a triangle.

Status families
StatusRamp500Text on whiteText on tint
Successgreen#22B35E fills, large marks#15803D 5.0:1#15803D on #EAF8EF 4.5:1
Errorred#F04438 fills, large marks#DC2626 4.8:1#B91C1C on #FEF1F1 5.8:1
Warningyellow#F5B300 fills, large marks#A36400 4.7:1#A36400 on #FFFAEB 4.5:1
Infoblue (shared)#00A7FF fills, large marks#0068C8 5.5:1#0068C8 on #EFF6FF 5.0:1

Semantic tokens

Components and screens only consume semantics. When a primitive shifts, semantics keep their names and everything downstream survives.

Semantic color tokens
TokenResolves toUsed for
uxp-color-text-primaryink-900Headings, body, values
uxp-color-text-secondaryink-500Helper text, captions, metadata
uxp-color-text-placeholderink-400Example text in inputs (documented exception)
uxp-color-text-disabledink-400 on bg-disabledDisabled controls
uxp-color-text-link, -hover, -visitedblue-600, blue-700, purple-700Links, always underlined
uxp-color-text-link-on-tintblue-700Links on any 50 tint: banners, selected rows
uxp-color-text-inverseink-0Text on blue fills and tooltips
uxp-color-bg-surface, -muted, -subtleink-0, ink-50, ink-100Page, footers and wells, chips
uxp-color-bg-selectedblue-600Current page, selected segment, active suggestion
uxp-color-bg-selected-subtleblue-50Selected rows, the current nav item, the bulk bar
uxp-color-bg-inverseink-700Tooltips
uxp-color-bg-disabledink-50Disabled control fills
uxp-color-bg-overlayink-900-a40 (ink-900 at 40%)Modal backdrop
uxp-color-border-subtleink-100Hairlines and dividers
uxp-color-border-input, -hoverink-200, ink-300Field edges (documented exception)
uxp-color-border-control, -hoverink-300, ink-400Checkbox, radio and switch edges (documented exception)
uxp-color-border-strongink-900Emphasis outlines
uxp-color-action-primary, -hoverblue-600, blue-700Primary buttons, checked controls, focus
uxp-color-action-secondary-bg, -bg-hoverblue-50, blue-100Secondary (tonal) button fill
uxp-color-action-secondary-textblue-700Secondary button text, current nav item, bulk-bar actions
uxp-color-action-on-primaryink-0Labels on action fills
uxp-color-action-destructive, -hoverred-700, red-800Destructive buttons and menu items
uxp-color-control-thumbink-400Switch thumb when off
uxp-color-status-*-fgstatus 700Status text and icons on white
uxp-color-status-*-tintstatus 50Banner and badge backgrounds
uxp-color-status-*-on-tintstatus 700, or red 800Text and icons on a tint
uxp-color-status-neutral-tint, -on-tintink-100, ink-700Neutral badges

Gradients

uxp-gradient-brand: 500 stops at 90°. The mark, full-bleed bands, decoration. Never behind text, never as text.
uxp-gradient-text: blue-600, purple-500, orange-600. Display text only; no point along it drops below 3.2:1 on white.
uxp-gradient-action: blue-600 to purple-600. The selected tab indicator, at 4.0:1 or more on white. Buttons stay flat.

Contrast

Every ratio is calculated from the hex values when the page is built, so this table can’t drift from the tokens.

Every pair a component uses, computed from the token values when this page was built
PairUsed forRatioWCAG 2.1 AAResult
ink-900 on whiteHeadings, body, values16.4:14.5:1Pass
ink-500 on whiteSecondary text and helper text5.0:14.5:1Pass
ink-500 on ink-50Secondary text on muted surfaces4.7:14.5:1Pass
ink-500 on blue-50Secondary text on selected rows4.6:14.5:1Pass
ink-400 on whitePlaceholder text3.2:14.5:1Exception
blue-600 on whiteLinks and tertiary buttons4.0:14.5:1Exception
blue-600 on ink-50Tertiary buttons and links on muted surfaces3.7:14.5:1Exception
white on blue-600Primary button, current page, selected segment4.0:14.5:1Exception
white on blue-700Primary button hover5.5:14.5:1Pass
blue-700 on whiteLink hover, info text5.5:14.5:1Pass
blue-700 on ink-50Tertiary and page-link hover on muted fills5.1:14.5:1Pass
blue-700 on red-50Links on tints (worst case across all 50 tints)4.9:14.5:1Pass
blue-700 on blue-50Secondary button, current nav item, info badge5.0:14.5:1Pass
blue-700 on blue-100Secondary button hover4.6:14.5:1Pass
purple-700 on whiteVisited links6.3:14.5:1Pass
white on red-700Destructive button4.8:14.5:1Pass
red-700 on whiteError text and icons, destructive menu items4.8:14.5:1Pass
white on ink-700Tooltips9.0:14.5:1Pass
ink-900 on ink-100Filter chips14.1:14.5:1Pass
green-700 on whiteSuccess text and icons5.0:14.5:1Pass
yellow-700 on whiteWarning text and icons4.7:14.5:1Pass
green-700 on green-50Success badge and banner icon4.5:14.5:1Pass
red-800 on red-50Error badge and banner icon5.8:14.5:1Pass
yellow-700 on yellow-50Warning badge and banner icon4.5:14.5:1Pass
ink-700 on ink-100Neutral badge7.8:14.5:1Pass
ink-900 on yellow-500Text on a warning fill8.8:14.5:1Pass
blue-600 on whiteDisplay text, gradient-text start4.0:13.0:1Pass
purple-500 on whiteDisplay text, gradient-text middle3.8:13.0:1Pass
orange-600 on whiteDisplay text, gradient-text end3.2:13.0:1Pass
blue-600 on whiteFocus ring, checked controls, current nav bar4.0:13.0:1Pass
blue-600 on blue-100Field focus edge against its halo3.3:13.0:1Pass
ink-400 on whiteSwitch thumb when off3.2:13.0:1Pass
ink-200 on whiteField edges1.4:13.0:1Exception
ink-300 on whiteCheckbox, radio and switch edges1.7:13.0:1Exception
blue-500 on whiteBrand mark and decoration only2.6:1LogotypeExempt
orange-500 on whiteBrand mark and decoration only2.2:1LogotypeExempt

Foundation 4 of 11

Typography

Three typefaces, each with one job. Satoshi does the talking, Inter does the working, JetBrains Mono does the labeling. Two faces would leave eyebrows, tokens and data nowhere to go but italics or all caps. Four would make someone choose between faces for buttons, and the system would lose its authority.

Satoshi

Display and headings. Loaded cuts: 500, 700, 900.

Inter

Body and every piece of interface text. Loaded: 400, 500, 600, 700.

JetBrains Mono

Eyebrows, token names, data. Loaded: 400, 500.

Nine font files. Every loaded weight is used by at least one text style, and nothing is synthesized.

Type scale

Orbit text styles, all prefixed uxp-text-
StyleFaceWeightSize / line height (px)TrackingRole
display-xlSatoshi70060 to 132 fluid / 0.98-0.025emCover title
display-lgSatoshi70048 to 72 fluid / 1.0-0.02emSection title
heading-1Satoshi70036 to 48 / 1.1-0.02emPage heading
heading-2Satoshi70028 to 36 / 1.15-0.015emSection heading
heading-3Satoshi50022 to 28 / 1.2-0.01emSub-section, card title
heading-4Inter70020 / 1.3-0.01emComponent heading
heading-5Inter60018 / 1.35-0.01emGroup heading
heading-6Inter60016 / 1.40Inline heading
body-lgInter40018 / 1.550Lead paragraph
body-mdInter40016 / 1.60Body
body-smInter40014 / 1.550Helper, error, caption text
captionInter50012 / 1.50.01emMetadata
overlineJetBrains Mono50012 / 1.40.04emEyebrows, sentence case
ui-button-smInter60013 / 160Small buttons
ui-button-mdInter60015 / 200Buttons, tabs
ui-button-lgInter60017 / 240Large buttons
ui-labelInter50014 / 200Field and control labels
ui-inputInter40016 / 240Typed values
mono-dataJetBrains Mono40013 / 20, tabular0Numbers, tokens, code in tables

UI text styles. Interface text needs sizes editorial text doesn’t: a 15px button label, a 14px field label, a 16px input. Rather than approximate them with body styles, they’re tokens, so every component uses a size the scale actually contains. Inputs are 16px, which stops iOS Safari zooming in on focus. Labels are 14px, because labels are the most-read text on a form.

Usage

Display once per page. Heading 1 once per page. Headings 2 and 3 structure sections. Headings 4 to 6 switch to Inter, which makes the hand-off from editorial to interface visible. Body-lg is for lead paragraphs only; it gets tiring after two. Overline is mono in sentence case and always sits above something: all caps at small sizes is the least legible text a system can ship.

Gradient title

design system.

uxp-text-style-title-gradient: Satoshi 900, uxp-gradient-text, background-clipped, with a solid ink-900 fallback and a forced-colors fallback.

The title gradient is built from 600 stops, not the 500 brand stops. Large text still needs 3:1, and the 500 blue and orange measure 2.6:1 and 2.2:1 on white.

Foundation 5 of 11

Spacing

A 4px base. Eleven steps for components and three for section rhythm. Every padding, margin and gap uses a step from the scale, so designers choose a step rather than a pixel value.

Spacing scale
TokenpxScale
uxp-space-14
uxp-space-28
uxp-space-312
uxp-space-416
uxp-space-520
uxp-space-624
uxp-space-832
uxp-space-1040
uxp-space-1248
uxp-space-1664
uxp-space-2080
uxp-space-section-sm64Alias of space-16. Between sub-blocks inside a section.
uxp-space-section-md96Between sections on mobile and tablet.
uxp-space-section-lg128Between sections on desktop.

Usage

Padding inside containers: the step that lets content breathe without crowding the border. Margin between flow elements: the smallest step that still separates them. Gap in flex and grid groups: usually the same step as the padding around the group.

Component spacing
WhereRule
Button paddingsm space-2 by space-4, md space-3 by space-6, lg space-3 by space-8
Card paddingspace-6 standard, space-8 feature (stat, quote)
Field stackingspace-2 between label, input and helper; space-6 between fields
Modalspace-6 around header and body, space-4 by space-6 in the footer
Menu and list itemsspace-2 by space-3 inside a 44px row
Section rhythmsection-md on tablet and mobile, section-lg on desktop

Foundation 6 of 11

Layout and direction

A 12-column grid that becomes 8 on tablet and 4 on mobile. Same logic on every surface, 1280px maximum content width. Past 1440px the page stops growing and the margins absorb the rest.

Breakpoints
TokenRange (px)ColumnsMarginGutter
uxp-bp-xs0 to 47941612
uxp-bp-sm480 to 76742416
uxp-bp-md768 to 102383220
uxp-bp-lg1024 to 1439126424
uxp-bp-xl1440 and up12auto24

The grid at lg: 12 columns, 24px gutters, 64px margins. At mobile widths, 4.

Grid exceptions

Three sanctioned cases: full-bleed brand bands, display type that extends into the outer margin at lg and up, and full-bleed photography. Anything else stays on the grid.

Reflow

Every layout works at 320px wide (400% zoom on a 1280px screen) without the page scrolling sideways. The only things allowed to scroll horizontally are two-dimensional content inside their own region: data tables and code.

Right-to-left

The Language switcher offers Arabic, so direction is a foundation, not a component detail. Layout uses logical properties only (margin-inline-start, never margin-left). Directional icons mirror: arrows, left and right chevrons, send. Checkmarks, clocks, media controls and anything depicting a real object never mirror. Numbers and code stay left to right inside right-to-left text.

Foundation 7 of 11

Shape

One firm radius for every control and container: 4px. Inner marks inside a control use 2px, and full rounding is kept for shapes that are round by nature. Hierarchy comes from elevation and weight, not from corners.

Radius and border tokens
TokenValueUsed for
uxp-radius-none0Full-bleed bands, tables inside scroll regions
uxp-radius-xs2pxMarks inside controls: menu and list rows, the selected segment, keyboard hints
uxp-radius-sm4pxEverything else: buttons, fields, checkboxes, badges, chips, cards, menus, modals, toasts, banners
uxp-radius-full999pxSwitch tracks
uxp-border-11pxHairlines, input edges, outlines
uxp-border-22pxFocus rings, checkbox, radio and switch edges, error state
uxp-border-33pxSelected tab indicator, current nav bar

Foundation 8 of 11

Elevation and layers

Five elevations, each describing how a surface relates to the page. Used sparingly, so elevation keeps its meaning.

elevation-0
Flat
elevation-1
Resting
elevation-2
Hover
elevation-3
Floating
elevation-4
Modal

Default is elevation-0 with a hairline. Elevation-1 is for surfaces that are genuinely interactive. Elevation-3 is for things that float: menus, the language list, search suggestions, tooltips. Shadows aren’t used on dark backgrounds or gradient bands, and cards that can’t be clicked don’t lift on hover. Shadows also disappear in forced-colors mode, so every floating surface carries a 1px transparent border that becomes visible there.

Layer tokens (z-index)
TokenValueUsed for
uxp-layer-base0Page content
uxp-layer-sticky100Sticky headers, sticky table columns
uxp-layer-dropdown200Menus, language list, search suggestions
uxp-layer-overlay300Non-modal overlays
uxp-layer-modal400Dialogs (native dialogs also use the top layer)
uxp-layer-toast500Toast region
uxp-layer-tooltip600Tooltips, always on top

Foundation 9 of 11

Motion

Motion in Orbit answers something a person did: opening, expanding, confirming, switching. Nothing moves on its own.

Motion tokens
TokenValueUsed for
uxp-motion-duration-xs100msOpacity changes; the only duration kept under reduced motion
uxp-motion-duration-sm160msHover, color and switch transitions
uxp-motion-duration-md280msAccordion, modal, toast, primary hover crossfade
uxp-motion-duration-lg400msLarge surfaces entering
uxp-motion-ease-standardcubic-bezier(0.2, 0, 0, 1)Most transitions
uxp-motion-ease-out-expocubic-bezier(0.22, 1, 0.36, 1)Things arriving: panels opening, toasts entering
uxp-motion-ease-incubic-bezier(0.4, 0, 1, 1)Things leaving

Under prefers-reduced-motion the sm, md and lg durations drop to zero, so changes are instant; xs survives for short fades that don’t move anything. Spinners slow down rather than stop, so loading is still visible.

Foundation 10 of 11

Icons

One set, drawn on a 24-unit grid with a 1.5px stroke and 2 units of inner padding. Functional, never decorative. Strokes stay 1.5px at every size (non-scaling stroke), so a 16px icon doesn’t thin out to a hairline.

16
20
24
32
48

Sizes: 16 inline, 20 UI, 24 navigation, 32 emphasis, 48 feature.

Accessibility

An icon next to a text label is decorative: aria-hidden="true" and focusable="false". An icon that is the only label of a control is named on the control (“Close”), not on the icon. A standalone icon that conveys information by itself gets role="img" and a label. Status icons are filled and have distinct shapes, so they read at a glance and never rely on hue.

Naming

icon-{noun} for outline, icon-{noun}-filled for filled. Direction is part of the noun: icon-arrow-right. A glyph enters the library after three shipped uses, the same entry criteria as components.

Navigation (8)
icon-arrow-left
icon-arrow-right
icon-chevron-up
icon-chevron-down
icon-chevron-left
icon-chevron-right
icon-menu
icon-close
Action (9)
icon-search
icon-download
icon-upload
icon-share
icon-edit
icon-copy
icon-plus
icon-trash
icon-external-link
Status (6)
icon-check
icon-check-circle-filled
icon-x-circle-filled
icon-alert-triangle-filled
icon-info-filled
icon-clock
Communication (4)
icon-link
icon-mail
icon-send
icon-message
Content and data (9)
icon-play
icon-pause
icon-image
icon-file
icon-calendar
icon-eye
icon-eye-off
icon-filter
icon-sort

The 36-glyph set. Arrows, left and right chevrons, and send mirror in right-to-left layouts.

Foundation 11 of 11

Writing

Interfaces are read before they’re used, so words are part of the system. Orbit’s voice is UXPective’s: direct and outcome-driven. Every element of text does one job, and every component’s examples on this page follow these rules.

Interface writing rules
ElementWriteNot
ButtonsThe verb for what happens: “Save changes”, “Submit application”“Submit”, “OK”, “Click here”
FlowsThe same verb all the way through: “Deprecate” produces “Token deprecated”“Deprecate” producing “Item removed”
LabelsThe thing being asked for: “Work email”“Enter your work email address below”
PlaceholdersAn example: “name@company.com”The label, or an instruction
Helper textWhy it’s asked, or the format: “We only use this to confirm your application.”A repeat of the label
ErrorsWhat happened and how to fix it: “Enter an email address with an @, like name@company.com.”“Invalid input”, “Oops!”, an apology
Empty statesWhat will appear and how to get it there“Nothing here”
LinksThe destination: “Read the contribution model”“Read more”, “Click here”
Alt textWhat the image shows and why it’s there“Image of”, or the file name
DirectionsThe thing itself: “Select Save changes”Position or color: “the button on the right”, “the red field”

Style

Sentence case everywhere, including headings, buttons and tabs. No exclamation marks in system messages. Curly quotes and apostrophes in text people read; straight ones only in code. No em dashes: split the sentence, or use a colon. Dates are written day, month, year in full (21 October 2026). Numbers are numerals, with ranges written as “1 to 4” in sentences and “1-4” in tight spaces. Plain words: short sentences and no idioms, because UXPective’s audience reads in more than one language.

Part 3 of 5

Twenty-two components

Every page below reads the same way: purpose, a live specimen, the spec, when to use it and when not to, the seven-row accessibility checklist, and the tokens it consumes. Every specimen is the real component, built from the tokens on this page, and every one can be used with a keyboard and a screen reader.

Components by group
GroupComponents
ActionsButton, Link, Menu
NavigationTabs, Side nav, Pagination, Language switcher
InputsText field, Select, Checkbox, Radio group, Switch, Segmented control, Search
FeedbackBanner, Toast, Badge, Tooltip
ContainersCard, Accordion, Modal
DataData table

Actions

Button

Starts an action on the current page: saving, submitting, opening, deleting. Four variants and three sizes.

Add a title to publish.
You have unsaved changes.

Press Save changes: the label stays put, a spinner appears, and a screen reader hears “Saving changes” then “Changes saved”. Publish is disabled but still focusable, so its reason can be read.Save changes keeps its label while saving: a spinner appears and a screen reader hears “Saving changes”, then “Changes saved”. Publish is disabled but still focusable, so its reason can be read.

Anatomy
Optional leading icon (16px), label (uxp-text-ui-button-md), optional trailing icon (16px), container (uxp-radius-sm, min-height not height).
Variants
Primary: solid uxp-color-action-primary with a white label. Secondary: tonal, a uxp-color-action-secondary-bg fill with uxp-color-action-secondary-text and no border. Tertiary: text only. Destructive: red-700 fill. One primary per view.
Sizes
sm 32px (desktop dense areas only: table rows, toolbars), md 44px (default), lg 52px (hero and checkout moments). Padding: sm space-2 by space-4, md space-3 by space-6, lg space-3 by space-8.
States
Default, hover (primary darkens to uxp-color-action-primary-hover; secondary’s fill deepens to uxp-color-action-secondary-bg-hover; tertiary gains a muted fill), active, focus-visible, disabled (aria-disabled), loading (spinner plus announcement, label unchanged).
Rationale
Buttons are flat. The gradient belongs to the brand mark and display type; on a control it made every action feel heavy. Actions use blue-600, a bright stop from the brand family, so the interface stays light. A label identifies a button on its own, so buttons carry no outline.

When to use

  • The control changes something on this page or submits data.
  • You need one clearly dominant action (primary) with quieter alternatives beside it.

When not to use

  • It goes to another URL. Use a Link, even when it is styled to look like a button (<a class="o-btn">).
  • There are three or more equal-weight actions. Use secondary or tertiary for all of them, or rethink the screen.
  • The action is destructive and irreversible without a confirmation. Pair it with a Modal (alertdialog).

Accessibility checklist

Button, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Primary: white on blue-600 4.0:1, a documented exception, on its hover 5.5:1. Destructive: white on red-700 4.8:1. Secondary: blue-700 on blue-50 5.0:1, on its hover 4.6:1. Tertiary: blue-600 on white 4.0:1, a documented exception.
Keyboard2.1.1Native <button>: Enter and Space activate. Link-styled buttons are real links and activate on Enter only.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. On dark or gradient bands use uxp-focus-ring-color-inverse.
Name, role, value4.1.2, 2.5.3The visible label is the accessible name. Icon-only buttons take an aria-label that is a verb (“Copy token name”, not “Copy icon”). Disabled uses aria-disabled="true", stays focusable, blocks activation in script and points to its reason with aria-describedby.
Announcements4.1.3Loading and result are announced through the page’s polite live region. aria-busy is not relied on, because most screen readers do not announce it on a button.
Reflow and text spacing1.4.10, 1.4.12min-height and padding, never a fixed height, so labels wrap instead of clipping under text-spacing overrides. Button bars stack full width below 480px.
Target sizebeyond AA44px at md and up meets AAA 2.5.5. WCAG 2.1 AA sets no target size, so sm at 32px is allowed, but only on desktop dense areas.

Tokens

  • uxp-color-action-primary
  • uxp-color-action-primary-hover
  • uxp-color-action-secondary-bg
  • uxp-color-action-secondary-bg-hover
  • uxp-color-action-secondary-text
  • uxp-color-action-on-primary
  • uxp-color-action-destructive
  • uxp-color-bg-disabled
  • uxp-color-text-disabled
  • uxp-focus-ring-color
  • uxp-target-md
  • uxp-target-sm
  • uxp-radius-sm
  • uxp-space-3
  • uxp-space-6
  • uxp-text-ui-button-md

Actions

Menu

A short list of actions on one thing, opened from a button. It keeps three or more secondary actions out of the way without hiding what they are.

Open with Enter, Space or the down arrow (the up arrow opens on the last item). Arrows move and wrap, Home and End jump, typing a letter moves to the matching item, Esc closes and returns focus to the button.Shown open. Enter, Space or the down arrow opens on the first item; arrows move and wrap, a letter jumps to the matching item, and Esc closes and returns focus to the button.

Anatomy
Trigger (a Button with a chevron, or a small “More” button), list (elevation-3, uxp-layer-dropdown), items (icon and label, 44px rows), optional separator, destructive item last.
Triggers
Labelled: “Draft actions” for a page-level set. Row overflow: a small “More” button named for its row (“More actions for Case Study Storytelling”), which is how the Data table holds a third row action.
States
Closed, open, item hover, item focus (fill plus inset ring), destructive item in red-700.
Rules
Two to seven items. Order by frequency; destructive last, after a separator. Items are verbs. A destructive item never acts on its own: it opens a confirmation.

When to use

  • One object has three or more secondary actions.
  • A table row needs more than its two inline actions.

When not to use

  • Items go to other pages. Use links, or Side nav.
  • It picks a value. Use Select or Radio group.
  • There are only one or two actions. Show them as buttons.

Accessibility checklist

Menu, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Items ink-900 16.4:1, destructive red-700 4.8:1; on the muted hover fill 15.3:1 and 4.5:1. Trigger follows Button.
Keyboard2.1.1, 2.1.2Enter, Space or Down opens on the first item; Up opens on the last. Arrows move and wrap, Home and End jump, a letter moves to the next matching item. Esc closes and returns focus; Tab closes and moves on.
Focus2.4.3, 2.4.7Roving focus inside the list; the focused item gets a fill and an inset ring. Closing always returns focus to the trigger. 2px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 2.5.3Trigger has aria-haspopup="menu", aria-expanded and aria-controls. The list is role="menu" labelled by its trigger; items are role="menuitem"; the separator is role="separator". A “More” trigger’s name starts with its visible word.
Announcements4.1.3The result of an action (“Link copied”) goes through the polite live region.
Reflow and text spacing1.4.10, 1.4.12Min width 220px, capped at the viewport; labels wrap inside 44px min-height rows.
Error prevention3.3.4Destructive items open a confirmation (Modal, alertdialog) and never act immediately.

Tokens

  • uxp-color-bg-surface
  • uxp-color-border-subtle
  • uxp-color-bg-muted
  • uxp-color-action-destructive
  • uxp-elevation-3
  • uxp-layer-dropdown
  • uxp-target-md
  • uxp-radius-sm
  • uxp-space-2
  • uxp-space-3

Navigation

Tabs

Switches between views of the same thing without leaving the page. Three to seven tabs, horizontal only: a vertical list of destinations is Side nav.

Overview panel. Who the cohort is for and how it runs.

Tab into the row, then use the arrow keys, Home and End. Tab again moves into the panel. Below 768px the row scrolls with a fade on the trailing edge.Arrow keys move between tabs, Home and End jump to the ends, and Tab moves into the panel. Below 768px the row scrolls with a fade on the trailing edge.

Anatomy
Tab list (role="tablist"), tabs (buttons), 3px active indicator in uxp-gradient-action, panels.
States
Default ink-500, hover ink-900, selected ink-900 at weight 700 plus indicator, focus-visible (inset ring).
Behavior
Roving tabindex. Arrows move and activate (automatic activation, because panels are already rendered). Home and End jump. Selection never relies on the indicator’s color alone: weight changes too.
Rationale
A vertical list in a sidebar almost always points at URLs, and a view with a URL is a page, not a tab.

When to use

  • Views are parallel and short, and nobody needs to link to one directly.

When not to use

  • Someone might bookmark or share a view. Use Side nav or separate pages.
  • There are two options that filter one list. Use Segmented control.
  • There are more than seven views.

Accessibility checklist

Tabs, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Unselected text ink-500 5.0:1, selected ink-900 16.4:1. Indicator gradient never drops below 4.0:1 on white, above the 3:1 a state indicator needs.
Keyboard2.1.1Tab enters the list at the selected tab, arrows move between tabs, Home and End jump, Tab leaves to the panel.
Focus2.4.3, 2.4.7Inset ring on the tab so it isn’t clipped by the scrolling row. Panels are focusable (tabindex="0") when they hold no focusable content.
Name, role, value4.1.2role="tab" with aria-selected and aria-controls; panels are role="tabpanel" with aria-labelledby; the list has an aria-label.
Announcements4.1.3Not needed. Screen readers announce the newly selected tab from its role and state.
Reflow and text spacing1.4.10, 1.4.12The row scrolls inside its own container at 320px rather than wrapping or pushing the page sideways. Tabs use min-height.
Use of color1.4.1Selected state is weight plus indicator, not hue.

Tokens

  • uxp-gradient-action
  • uxp-color-text-primary
  • uxp-color-text-secondary
  • uxp-color-border-subtle
  • uxp-border-3
  • uxp-target-md
  • uxp-text-ui-button-md

Navigation

Side nav

Moves between pages or sections that each have their own URL, such as documentation.

These are real links to this page’s foundations. Color is marked as the current page.

Anatomy
<nav> landmark, optional group title, list of links, current item.
States
Default, hover (muted fill), current (blue-50 fill, blue-700 text at weight 600, and a 3px blue-600 bar at the inline start), focus-visible.

When to use

  • Every item has its own URL or anchor.
  • The list is long enough to need persistent wayfinding (docs, settings areas).

When not to use

  • Items swap content inside one view. Use Tabs.
  • There are fewer than three destinations. Use links.

Accessibility checklist

Side nav, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Items ink-900 16.4:1. Current item blue-700 on blue-50 5.0:1, marked by a 3px blue-600 bar (3.6:1 against its fill), so the state never rests on a tint alone.
Keyboard2.1.1Plain links in a list: Tab moves through them, Enter follows.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 1.3.1<nav> labelled by its visible title. Current item carries aria-current="page", never just a color.
Announcements4.1.3Not applicable. Navigation loads the destination.
Reflow and text spacing1.4.10, 1.4.12Below 1024px the side nav collapses into a disclosure at the top of the content, so it never forces horizontal scroll.
Consistent navigation3.2.3Same order on every page it appears on.

Tokens

  • uxp-color-bg-selected-subtle
  • uxp-color-action-secondary-text
  • uxp-color-action-primary
  • uxp-color-bg-muted
  • uxp-border-3
  • uxp-target-md
  • uxp-radius-sm

Navigation

Pagination

Moves through a long, ordered set. Two patterns: numbered and load more. There is no infinite scroll.

  • Recording 1
  • Recording 2
  • Recording 3

Showing 3 of 9

Numbered updates aria-current and announces “Page 3 of 12”. Load more keeps focus on the button and announces how many arrived; once everything is shown, focus moves to the first new item.Changing page updates aria-current and announces “Page 3 of 12”. Load more keeps focus on the button and announces how many arrived; when the last batch lands, focus moves to the first new item.

Anatomy
<nav aria-label="Pagination">, previous and next links, page links, gap marker, status line.
Patterns
Numbered for tables and lists where position matters; the URL carries ?page=3. Load more for lists with a clear end. Below 480px numbered collapses to previous, “3 of 12”, next.
States
Default, hover (muted fill, blue-700 text), current (blue-600 fill, white text), focus-visible. Previous is omitted on page 1 rather than disabled.
Rationale
It makes the footer unreachable by keyboard and gives screen reader users no sense of position. Feeds use load more.

When to use

  • The set is longer than one screen and has an order.

When not to use

  • The set is short enough to show in full (under about 24 items).
  • You want infinite scroll. It is not offered; use load more.

Accessibility checklist

Pagination, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Page links blue-600 4.0:1, a documented exception, blue-700 on the muted hover fill 5.1:1. Current page white on blue-600 4.0:1, a documented exception. Status ink-500 5.0:1.
Keyboard2.1.1All links and buttons. Load more never traps focus and the footer stays reachable.
Focus2.4.3, 2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. Load more keeps focus on itself while items remain; when the last batch lands, focus moves to the first new item.
Name, role, value4.1.2, 2.4.4, 1.3.1Previous and next carry hidden text (“Previous page”). Page links read “Page 3”. The current page has aria-current="page". The gap is aria-hidden.
Announcements4.1.3“Page 3 of 12” and “3 more loaded, showing 6 of 9” go through the polite live region.
Reflow and text spacing1.4.10, 1.4.12Compact pattern below 480px; 44px targets at every size.
Consistent identification3.2.4Previous and next always sit at the ends with the same icons.

Tokens

  • uxp-color-text-link
  • uxp-color-action-primary-hover
  • uxp-color-bg-selected
  • uxp-color-text-inverse
  • uxp-color-text-secondary
  • uxp-color-bg-muted
  • uxp-target-md
  • uxp-radius-sm

Navigation

Language switcher

Opens the same page in another language. A disclosure button with a list of links, because changing language is navigation, and navigation is a link.

Each option is a link with lang and hreflang, so a screen reader switches voice per option. Esc closes and returns focus to the button. In this demo, choosing a language shows a toast instead of navigating.Shown open. Each option is a link with lang and hreflang, so a screen reader switches voice per option. Esc closes the list and returns focus to the button.

Anatomy
Trigger button (“Language: English”, chevron), list of links (code, native name), current marker.
Content rules
Names are written in their own language and script. Never flags: a flag is a country, not a language. Codes are decorative; the native name carries meaning.
States
Closed, open, option hover, current option (weight 600, a check icon and aria-current), focus-visible.
Direction
Right-to-left names render with dir="rtl". The destination page sets lang and dir on <html>, so no extra announcement is needed.

When to use

  • A page exists in more than one language.

When not to use

  • You are choosing a language as a form value (for example, “preferred language” in a profile). Use Select.
  • You want a flag picker.

Accessibility checklist

Language switcher, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Trigger follows secondary Button. Options ink-900 16.4:1, codes ink-500 5.0:1, and both hold on the muted hover fill (codes 4.7:1).
Keyboard2.1.1, 2.1.2Enter or Space opens; Tab moves through options; Esc closes and returns focus to the trigger; clicking outside closes.
Focus2.4.3, 2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 2.5.3Trigger name “Language: English” contains its visible text. aria-expanded and aria-controls on the trigger. The current option has aria-current="true".
Announcements4.1.3Not needed. The new page announces its own language through <html lang>.
Reflow and text spacing1.4.10, 1.4.12List min-width 240px and capped at the viewport; long names wrap.
Language of parts3.1.2Every option carries lang, so “العربية” is read with an Arabic voice. Switching happens only when a link is followed, never while arrowing (3.2.2).

Tokens

  • uxp-color-bg-surface
  • uxp-color-border-subtle
  • uxp-elevation-3
  • uxp-layer-dropdown
  • uxp-target-md
  • uxp-color-text-secondary

Inputs

Text field

Collects free text: names, emails, URLs, longer answers. The component that was rebuilt three times before Orbit existed. One component, one validation pattern, every form.

Apply for a cohort

We only use this to confirm your application.

Starts with https://. You can paste the full address.

280 characters left

Leave a required field empty, or type an email without an @, then press Tab: the error appears on blur, never mid-keystroke. Submit with errors and focus moves to a summary that links to each field. Bottom row: disabled, read-only, and the one-time-code configuration.Errors appear on blur, never mid-keystroke. Submitting with errors moves focus to a summary that links to each field. Bottom row: disabled, read-only, and the one-time-code configuration.

Anatomy
Label (uxp-text-ui-label, 14/500, ink-900), “(required)” or “(optional)” in ink-500, input (uxp-text-ui-input, 16/400), optional prefix, helper (body-sm), error (icon plus body-sm in red-700), character count.
Sizes
sm 32px, md 44px (default), lg 52px. Same heights as Button, so a field and a button side by side line up.
Configurations
Multiline: textarea, resizable vertically. Date: native type="date" until a date picker earns its place. One-time code: inputmode="numeric" and autocomplete="one-time-code".
States
Default (ink-200 edge), hover (ink-300), focus (2px blue-600 edge and a soft halo), filled, error (2px red-700 edge, icon, message), disabled (muted fill, hairline edge), read-only (dashed edge on ink-50).
Validation
On blur, never while typing. On submit, a summary at the top lists every error as a link to its field, and focus moves to the summary. Messages say what to do: “Enter an email address with an @, like name@company.com”.
Rationale
Fields are light: a near-white ink-200 edge, 4px corners, a 44px height and 16px of inner padding. Each field has a visible label above it, and focus turns the edge blue. Input text is 16px so iOS Safari doesn’t zoom in on focus; labels are 14px because they’re the most-read text on a form.

When to use

  • The answer is free text.

When not to use

  • The answer is one of a known short list. Use Radio group (up to 6) or Select.
  • You need a label. Placeholders are examples, never labels, and they vanish the moment someone types.

Accessibility checklist

Text field, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Value ink-900 16.4:1, helper ink-500 5.0:1, error red-700 4.8:1. Placeholder ink-400 3.2:1, a documented exception. Edge ink-200 1.4:1, a documented exception.
Keyboard2.1.1Native inputs. Error summary links move focus to their field.
Focus2.4.3, 2.4.7Focus turns the edge into a 2px blue-600 line (4.0:1) with a soft uxp-focus-halo around it, and a transparent outline that becomes solid in forced-colors mode. On submit with errors, focus moves to the summary.
Name, role, value4.1.2, 1.3.1<label for> on every field. Helper and error joined through aria-describedby. Invalid fields set aria-invalid="true". Required fields use the required attribute plus visible “(required)” text.
Announcements4.1.3The character count is announced through the polite live region at 50, 20 and 0 characters left, not on every keystroke.
Reflow and text spacing1.4.10, 1.4.12Fields are full width up to 420px and never set a fixed height.
Input purpose and errors1.3.5, 3.3.1, 3.3.2, 3.3.3autocomplete on every personal field (name, email, url, one-time-code). Errors are identified in text, not just a red border, and each message suggests the fix.

Tokens

  • uxp-color-border-input
  • uxp-color-border-input-hover
  • uxp-focus-halo
  • uxp-color-text-placeholder
  • uxp-color-status-error-fg
  • uxp-color-bg-disabled
  • uxp-color-bg-muted
  • uxp-text-ui-label
  • uxp-text-ui-input
  • uxp-text-body-sm
  • uxp-target-md
  • uxp-radius-sm
  • uxp-space-2
  • uxp-space-4
  • uxp-border-2

Inputs

Select

Picks one value from a list too long for radios.

Used to show prices in your currency.

A styled native select. The open list is the platform’s own, which is the most reliable listbox there is on phones and with screen readers.

Anatomy
Label, native <select> in a wrapper that draws the chevron (an icon mask filled with uxp-color-text-primary), helper, error. Same label, helper and error rules as Text field.
Sizes
sm 32px and md 44px, matching Text field and Button.
States
Default, hover, focus, error, disabled. Forced-colors mode falls back to the native arrow.
Decision
Native, not custom. A custom listbox is only worth building for rich options (icons, two-line items) and no shipped screen needs that yet.

When to use

  • There are more than six options, or the options are a standard list (countries, months).

When not to use

  • There are six or fewer options and they fit on screen. Use Radio group: every option stays visible.
  • Choosing navigates somewhere. Use links (see Language switcher).
  • People need to pick several. Use Checkbox.

Accessibility checklist

Select, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Text ink-900 16.4:1; edge ink-200 1.4:1, a documented exception; chevron uxp-color-text-primary 16.4:1.
Keyboard2.1.1Native: arrows, type-ahead, Enter, Esc.
Focus2.4.7Focus turns the edge into a 2px blue-600 line (4.0:1) with a soft uxp-focus-halo around it, and a transparent outline that becomes solid in forced-colors mode.
Name, role, value4.1.2, 1.3.1<label for>; helper and error via aria-describedby; aria-invalid on error.
Announcements4.1.3Not needed; the platform announces the value.
Reflow and text spacing1.4.10, 1.4.12Full width to 420px; min-height.
On input3.2.2Changing the value never submits a form or changes the page. Some browsers fire change while arrowing through a closed select, which is exactly why a select must never navigate.

Tokens

  • uxp-color-border-input
  • uxp-color-text-primary
  • uxp-focus-halo
  • uxp-text-ui-input
  • uxp-target-md
  • uxp-radius-sm

Inputs

Checkbox

Turns one or more independent options on or off.

Email me about

Pick at least one.

Select all shows the mixed state when some topics are on. Clear every topic to see the group error, which is attached to the fieldset.Select all shows the mixed state while some topics are on. Clearing every topic shows the group error, which is attached to the fieldset.

Anatomy
20px box (2px ink-300 edge, uxp-radius-sm), check or mixed bar, label. The whole row is the hit area, at least 44px tall.
States
Unchecked, checked (blue-600 fill, white mark), mixed, hover (ink-400 edge), focus-visible, disabled, error (red-700 edge on the group’s boxes plus a message).
Groups
Every group is a <fieldset> with a <legend>. The rule (“Pick at least one”) sits above the options; the error sits below them.

When to use

  • Options are independent, or several can be on at once.
  • A single option is a yes or no that takes effect on submit (“Email me a copy”).

When not to use

  • Only one option can be chosen. Use Radio group.
  • The setting takes effect immediately. Use Switch.

Accessibility checklist

Checkbox, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Box edge ink-300 1.7:1, a documented exception; checked fill blue-600 4.0:1; white mark on blue-600 4.0:1; labels ink-900 16.4:1.
Keyboard2.1.1Native: Tab to each box, Space toggles.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 1.3.1Wrapping <label>; mixed state via indeterminate (read as “mixed” or “half checked”); fieldset and legend name the group; rule and error joined to the fieldset through aria-describedby.
Announcements4.1.3The group error is announced through the polite live region when it appears.
Reflow and text spacing1.4.10, 1.4.12Rows wrap; the label grows, the box stays 20px.
Error identification3.3.1, 3.3.2The rule is stated before anyone can break it, and the error says what to do.

Tokens

  • uxp-color-border-input
  • uxp-color-action-primary
  • uxp-color-action-on-primary
  • uxp-color-status-error-fg
  • uxp-color-bg-muted
  • uxp-target-md
  • uxp-border-2
  • uxp-radius-sm

Inputs

Radio group

Picks exactly one option from a short list where every option should stay visible. Segmented control is built on it.

Which cohort are you applying for?

You can change this until applications close.

Tab enters the group at the selected option; arrow keys move the selection.Tab enters the group at the selected option; arrow keys move the selection.

Anatomy
Fieldset, legend, optional rule, 20px circle (2px ink-300 edge), 10px blue-600 dot, label.
States
Unselected, selected, hover, focus-visible, disabled, error.
Rules
Two to six options. Stack vertically. Preselect only when there is a safe default.

When to use

  • Exactly one choice from two to six options.

When not to use

  • More than six options. Use Select.
  • The options switch a view rather than set a value. Use Segmented control or Tabs.

Accessibility checklist

Radio group, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Circle edge ink-300 1.7:1, a documented exception; dot blue-600 4.0:1; labels ink-900 16.4:1.
Keyboard2.1.1Native: Tab into the group, arrows change the selection.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 1.3.1Native radios sharing a name; fieldset and legend; rule joined with aria-describedby.
Announcements4.1.3Not needed; selection is announced natively.
Reflow and text spacing1.4.10, 1.4.12Stacked rows, labels wrap.
Labels3.3.2The legend asks the question in full.

Tokens

  • uxp-color-border-input
  • uxp-color-action-primary
  • uxp-target-md
  • uxp-border-2

Inputs

Switch

Turns a single setting on or off, and the change takes effect immediately. If it needs a Save button, it’s a Checkbox.

Notifications

Tab to a switch and press Space. A screen reader hears “Captions on recordings, switch, off”, then “on”. The change is confirmed through the live region because it takes effect immediately.Space toggles a switch, and a screen reader hears “switch, on” or “off”. The change applies immediately and is confirmed through the live region.

Anatomy
Label, optional hint, track (44 by 24px, uxp-radius-full, 2px border), thumb (16px). The label sits at the inline start; the switch at the inline end.
States
Off (white track, ink-300 edge, ink-400 thumb), on (blue-600 track, white thumb), hover, focus-visible, disabled (with its reason as the hint).
Behavior
Applies on toggle, with no Save step. If saving can fail, the switch returns to its previous state and an inline error says why.
Groups
Related switches share a fieldset and legend, separated by hairlines.

When to use

  • A setting takes effect the moment it changes (notifications, captions, preferences).

When not to use

  • The choice is submitted with a form. Use Checkbox.
  • There are more than two states. Use Radio group or Segmented control.
  • The label would need to say what “on” means. Rewrite the label so on and off are obvious.

Accessibility checklist

Switch, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Off: ink-300 edge 1.7:1, a documented exception and ink-400 thumb 3.2:1 against white. On: blue-600 track 4.0:1 against the page, white thumb 4.0:1 against the track. Labels ink-900.
Keyboard2.1.1A native checkbox underneath: Tab to it, Space toggles.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. The ring wraps the track.
Name, role, value4.1.2<input type="checkbox" role="switch"> inside its label, read as “switch, on” or “off”. Hints and disabled reasons are joined with aria-describedby.
Announcements4.1.3Because the change applies immediately, the result (“Captions turned on”) is confirmed through the polite live region; a failure is announced and shown inline.
Reflow and text spacing1.4.10, 1.4.12Labels wrap; the switch keeps its size at the inline end; rows use min-height.
Use of color1.4.1State is the thumb’s position as well as the fill. Forced-colors mode falls back to the native checkbox.

Tokens

  • uxp-color-action-primary
  • uxp-color-action-on-primary
  • uxp-color-border-input
  • uxp-color-control-thumb
  • uxp-color-bg-disabled
  • uxp-radius-full
  • uxp-border-2
  • uxp-motion-duration-sm
  • uxp-target-md

Inputs

Segmented control

Switches between two to four mutually exclusive modes of one view, like table density.

Show sessions

A radio group underneath, so arrow keys move the selection and screen readers announce “Past, radio button, 2 of 3”.A radio group underneath, so arrow keys move the selection and a screen reader announces “Past, radio button, 2 of 3”.

Anatomy
Fieldset and legend, track (1px ink-200 edge, uxp-radius-sm), segments (native radios, visually replaced), selected segment (blue-600 fill, white text).
States
Default, hover, selected, focus-visible (ring on the segment).
Rules
Two to four segments, short labels, no icons without text. Changing a segment updates the view in place and never navigates.

When to use

  • Two to four modes of the same content (density, list or grid, upcoming or past).

When not to use

  • The modes have their own URLs. Use Tabs or Side nav.
  • It sets a form value. Use Radio group.

Accessibility checklist

Segmented control, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Labels ink-900 16.4:1; selected white on blue-600 4.0:1, a documented exception; the selected fill itself 4.0:1 against the track.
Keyboard2.1.1Native radio behavior: Tab in, arrows move.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. The ring is drawn on the visible segment.
Name, role, value4.1.2Native radios in a fieldset with a legend; the selected state is native checked.
Announcements4.1.3If the change updates content elsewhere (a table), announce the result (“Showing compact rows”).
Reflow and text spacing1.4.10, 1.4.12The track wraps at 320px.
Use of color1.4.1Selected is a filled segment, a change in both fill and text color, not hue alone.

Tokens

  • uxp-color-bg-selected
  • uxp-color-text-inverse
  • uxp-color-border-input
  • uxp-target-md
  • uxp-radius-sm

Feedback

Banner

An inline message about the page or section it sits in. It stays until the situation changes or someone dismisses it.

Every token follows one naming pattern

The token foundation shows how names are built.

Tokens exported

Figma variables and CSS custom properties are in sync.

This component is in beta

Details may change before it’s marked stable. Tell the owner before using it on a shipped screen.

Export failed

Two tokens point at a primitive that doesn’t exist. Check the naming rules

Each status has its own icon shape, so the message never depends on hue. Dismissing the first banner moves focus to the control that replaces it, instead of dropping focus on the page body.

Anatomy
Status icon (20px, distinct shape per status), title, text, optional action link, optional dismiss button, on the status tint with uxp-radius-sm. No border and no stripe: the icon and title carry the status.
Variants
Info (circle i), success (circle check), warning (triangle), error (circle x).
Links
Links inside a banner use uxp-color-text-link-on-tint and stay underlined.
Placement
At the top of the region it describes. Page-level banners sit under the header; form-level banners above the form.
Live behavior
Banners present on load have no live role. A banner inserted after an action uses role="status" (info, success) or role="alert" (error only).

When to use

  • The message is about a whole page or section and stays true until something changes.

When not to use

  • It confirms an action someone just took. Use Toast.
  • It is about one field. Use the field’s error message.
  • It is promotional. Banners are for state, not marketing.

Accessibility checklist

Banner, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Text ink-900 on every tint (14.9:1 or better). Links use uxp-color-text-link-on-tint, blue-700 (4.9:1 or better). Icons use the status -on-tint tokens: success 4.5:1, error 5.8:1, warning 4.5:1, info 5.0:1.
Keyboard2.1.1Links and dismiss are native controls.
Focus2.4.3, 2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. On dismiss, focus moves to the next logical control, never to <body>.
Name, role, value4.1.2, 1.1.1Dismiss is named for what it dismisses (“Dismiss: Export failed”). Status icons are decorative because the title states the status in words.
Announcements4.1.3Inserted banners use role="status", or role="alert" for errors that block progress.
Reflow and text spacing1.4.10, 1.4.12Grid layout; text wraps; no fixed height.
Use of color1.4.1Status is carried by icon shape and title, not by tint. Warning is yellow (48°), well away from brand orange (31°), and always shows the triangle.

Tokens

  • uxp-color-status-info-tint
  • uxp-color-status-info-on-tint
  • uxp-color-status-success-tint
  • uxp-color-status-warning-tint
  • uxp-color-status-error-tint
  • uxp-color-text-link-on-tint
  • uxp-radius-sm

Feedback

Toast

A short confirmation of something someone just did. It never carries the only copy of important information.

A toast without an action stays at least 6 seconds and pauses while hovered or focused. A toast with Undo stays until it is used or dismissed. Neither steals focus.Shown: a toast with Undo, which stays until it is used or dismissed. Toasts without an action stay at least 6 seconds and pause while hovered or focused. Neither takes focus.

Anatomy
Message, optional single action (Undo), dismiss button. White surface with a hairline and elevation-3, uxp-radius-sm, inline-end bottom corner, uxp-layer-toast.
Timing
Without an action: at least 6 seconds, paused on hover and focus, dismissible. With an action: persistent until acted on or dismissed.
Wording
Past tense of the action’s own verb: the button that says “Deprecate” produces “Token deprecated”.
Stacking
Up to three, newest at the bottom. More than three means the flow is too noisy.

When to use

  • Confirming an action that just happened, especially one that can be undone.

When not to use

  • The message must be read, or it requires a decision. Use Banner or Modal.
  • It reports an error. Errors go inline, next to what caused them.

Accessibility checklist

Toast, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Message ink-900 16.4:1 on white. Undo is a tertiary button, blue-600 4.0:1, a documented exception. The surface is lifted by elevation, not by a dark fill.
Keyboard2.1.1Undo and dismiss are buttons, reachable with Tab. The toast region sits at the end of the page in DOM order.
Focus2.4.3, 2.4.7Focus is never moved to a toast. Its buttons use the standard focus ring.
Name, role, value4.1.2Dismiss reads “Dismiss notification”. Undo reads “Undo”, followed by the message through the live region.
Announcements4.1.3The message is announced through the system’s polite live region, which exists on page load, so it is read reliably. The buttons stay out of the announcement.
Reflow and text spacing1.4.10, 1.4.12Max width 400px, full width minus margins below 480px.
Timing2.2.1Nothing essential auto-dismisses. Anything with an action waits for the person; anything without one pauses while they’re looking at it.

Tokens

  • uxp-color-bg-surface
  • uxp-color-text-primary
  • uxp-color-border-subtle
  • uxp-elevation-3
  • uxp-layer-toast
  • uxp-radius-sm
  • uxp-motion-duration-md
  • uxp-motion-ease-out-expo

Feedback

Badge

A short, static label for status or category.

Stable Beta Draft Deprecated Failing Component

Text on a tint uses the status -on-tint token, which is why Beta reads at 5.0:1.

Anatomy
Label (12/600) on a status tint, uxp-radius-sm. No dot: the word carries the status.
Variants
Neutral (uxp-color-status-neutral-tint and -on-tint), info, success, warning, error (status tint and -on-tint).
Lifecycle mapping
Orbit’s own docs use Draft (neutral), Beta (info), Stable (success), Deprecated (warning).

When to use

  • A short label helps someone scan a list or table.

When not to use

  • It is clickable. Use a Button or Link; a filter chip lives in Search.
  • The label needs more than two words.

Accessibility checklist

Badge, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3Text on tint: success 4.5:1, error 5.8:1, warning 4.5:1, info 5.0:1, neutral ink-700 on ink-100 7.8:1.
Keyboard2.1.1Not applicable. Badges are static text and are not focusable.
Focus2.4.7Not applicable.
Name, role, value4.1.2, 1.3.1Plain text. The dot is aria-hidden. In tables, the column header gives the badge its context.
Announcements4.1.3If a badge changes after load (Draft to Stable), announce the change through the live region.
Reflow and text spacing1.4.10, 1.4.12No fixed width; padding, not height.
Use of color1.4.1Every badge has a text label. Color is a second cue, never the only one.

Tokens

  • uxp-color-status-neutral-tint
  • uxp-color-status-neutral-on-tint
  • uxp-color-status-success-tint
  • uxp-color-status-success-on-tint
  • uxp-radius-full
  • uxp-space-1
  • uxp-space-3

Feedback

Tooltip

A short text label or description that appears on hover and focus. Never interactive, never the only place information lives.

Hover or Tab to each button. The first tooltip is the icon button’s name; the second adds a description. Esc hides a tooltip without moving focus, and the pointer can move onto it without it vanishing.Both tooltips shown. The first is the icon button’s name; the second adds a description. Esc hides a tooltip without moving focus, and the pointer can move onto it without it disappearing.

Anatomy
Trigger (a focusable control), bubble (ink-700, 13/500, uxp-radius-sm), 8px gap with an invisible bridge so the pointer can cross it.
Uses
Label: names an icon-only button (aria-labelledby). Description: adds a short note to a labelled control (aria-describedby).
Behavior
Shows on hover after 300ms and immediately on focus. Hides on blur, on pointer leave (after 100ms), and on Esc.
Touch
Touch has no hover, so anything a touch user needs must also be visible as text.

When to use

  • An icon-only button needs a visible name on hover.
  • A control benefits from one sentence of extra context.

When not to use

  • It contains a link, a button, or anything to click. Use Menu for actions; other interactive content needs a popover, which is in the backlog.
  • The information is required to complete a task. Put it on the page.

Accessibility checklist

Tooltip, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3White on ink-700 9.0:1.
Keyboard2.1.1Appears on keyboard focus, not just hover.
Focus2.4.7The trigger keeps its own focus ring; the tooltip never takes focus.
Name, role, value4.1.2role="tooltip", connected with aria-labelledby (label use) or aria-describedby (description use).
Announcements4.1.3Not needed; the relationship attribute does the work.
Reflow and text spacing1.4.10, 1.4.12Bubble wraps at 240px when space is tight.
Content on hover or focus1.4.13Dismissible (Esc), hoverable (pointer can move onto it), persistent (stays until hover or focus leaves).

Tokens

  • uxp-color-bg-inverse
  • uxp-color-text-inverse
  • uxp-layer-tooltip
  • uxp-elevation-3
  • uxp-radius-sm

Containers

Card

Groups one item’s content so it can sit in a collection. Four variants, because the structure inside genuinely differs.

Brand gradient

Image-first card. Crops at 16:9.

182

tokens in Orbit

Values, text styles and breakpoints.

“Decisions are made once in the system, so they aren’t made again on every screen.”
Orbit, the gradient problem

The first card is clickable: its title is the link, stretched over the card. Nothing else on the card is hidden from screen readers.

Anatomy
Container (1px ink-100 hairline, uxp-radius-sm), optional media (16:9), title (<h4> or the level that fits the page), body, meta.
Variants
Content, media, stat, quote. Stat and quote are feature cards.
Padding
Standard: uxp-space-6 (24px). Feature: uxp-space-8 (32px). Media image: none, full bleed.
Clickable cards
The title is the only link. Its ::after stretches over the card, so the whole card is the hit area while the name stays the title. Focus ring draws on the stretched area. Resting elevation-1, hover elevation-2. Cards never nest.

When to use

  • Several items of the same kind sit side by side and each needs its own boundary.

When not to use

  • There is only one item. That is section content.
  • The border is there to make static text feel designed.
  • It’s used to make thin content look substantial.

Accessibility checklist

Card, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Title ink-900 16.4:1, body and meta ink-500 5.0:1. The ink-100 hairline is decorative: the card’s identity comes from its title, so 1.4.11 does not require 3:1 here.
Keyboard2.1.1One tab stop per clickable card (the title link). Cards with several actions are not clickable as a whole.
Focus2.4.7Ring on the stretched link area, 2px blue-600 at 2px offset.
Name, role, value4.1.2, 1.3.1Cards are <article>; quotes are <figure>, <blockquote> and <figcaption>. Nothing is aria-hidden except decoration.
Announcements4.1.3Not applicable.
Reflow and text spacing1.4.10, 1.4.12Grid of minmax(240px, 1fr) collapses to one column; cards grow with content.
Non-text content1.1.1Media that carries meaning gets alt text; decorative media gets empty alt. The media card above describes what the image shows.

Tokens

  • uxp-color-bg-surface
  • uxp-color-border-subtle
  • uxp-elevation-1
  • uxp-elevation-2
  • uxp-space-6
  • uxp-space-8
  • uxp-radius-sm
  • uxp-focus-ring-color

Containers

Accordion

Shows and hides sections of long, parallel content. Built on the heading-plus-button pattern so each section is a real heading.

When the content earns the visual weight of the page around it. A card with eight items in it is a section that hasn’t noticed it is a section.

Single-expand. Each question is a heading, so screen reader users can jump between them with the H key. Animation uses the motion tokens and collapses to an instant change under reduced motion.Single-expand. Each question is a heading, so screen reader users can jump between them. Motion uses the motion tokens and becomes instant under reduced motion.

Anatomy
Heading (level that fits the page) wrapping a button, chevron (decorative), panel region.
Variants
Single-expand for FAQs: opening one closes the others. Multi-expand for dense settings or syllabi where people compare panels.
Motion
Panel animates grid-template-rows from 0fr to 1fr over uxp-motion-duration-md (280ms) on uxp-motion-ease-out-expo; the chevron shares the curve.
Rationale
A heading inside <summary> loses its heading role in several screen readers. Eyebrow labels never go inside the button, where they would be read as part of every name.

When to use

  • Parallel sections are long, and people need one or two at a time.

When not to use

  • Panels are short. Use Tabs, or just show them.
  • Someone might link to a panel. Make it a page.
  • Most people need every panel. Hiding it all is a cost, not a saving.

Accessibility checklist

Accordion, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Questions ink-900 16.4:1, answers ink-500 5.0:1, chevron ink-900.
Keyboard2.1.1Each header is a native button: Tab moves between them, Enter or Space toggles.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2, 1.3.1Heading wraps a button with aria-expanded and aria-controls. Panels are labelled regions and are hidden when closed, so their content leaves the reading order.
Announcements4.1.3Not needed; expanded state is announced from aria-expanded.
Reflow and text spacing1.4.10, 1.4.12The grid-rows technique never measures height, so enlarged text can’t be clipped by a stale pixel value.
Motionbeyond AAReduced motion makes the change instant (2.3.3 is AAA; Orbit does it by default).

Tokens

  • uxp-motion-duration-md
  • uxp-motion-ease-out-expo
  • uxp-color-border-subtle
  • uxp-target-md
  • uxp-color-text-secondary

Containers

Modal

Interrupts to ask for a decision or a short task, then gets out of the way. Built on the native <dialog> element.

Delete this draft?

The draft and its comments will be removed. This can’t be undone.

Contributing a component

Bring screenshots of the pattern on three shipped screens. If it passes the entry criteria, build it in Figma and code with matching names, write its docs from the template, and fill every row of its WCAG 2.1 AA checklist. An accessibility reviewer who didn’t build it signs it off.

This is a large modal: clicking the backdrop does not close it, so an accidental click can’t lose someone’s place in a long read. Esc and the close button still work.

The delete confirmation is an alertdialog and opens on “Keep editing”, the safe choice. Focus returns to the button that opened it. The background is inert while either is open.Shown: the delete confirmation, an alertdialog that opens on “Keep editing”, the safe choice. Focus returns to the button that opened it, and the page behind is inert while it is open.

Anatomy
Backdrop (uxp-color-bg-overlay), container (uxp-radius-sm, elevation-4, 1px transparent border that becomes visible in forced-colors mode), header with title and close, body (max 60ch), footer (ink-50, actions at the inline end).
Sizes
sm 400px (confirmations), md 560px (short forms), lg 800px (reading). Below 480px all sizes go full width with 16px margins and footer buttons stack; lg becomes a full-screen sheet.
Behavior
Opens with showModal(), which makes the rest of the page inert. Esc closes. Backdrop click closes sm and md only. Body scrolls inside, the page doesn’t. Focus returns to the trigger, or to a sensible fallback if the trigger is gone.
Initial focus
Destructive confirmations: the safe action. Forms: the first field. Reading: the close button.

When to use

  • A decision has to be made before continuing.
  • A task is short enough to finish in one or two steps.

When not to use

  • The task has three or more steps. Give it a page with a URL; modals don’t survive a reload.
  • You’re warning about something they aren’t doing. Use Banner.
  • You’re confirming something that already happened. Use Toast.
  • You’re upselling. Use a section.

Accessibility checklist

Modal, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Title ink-900, body ink-900, footer actions per Button. Backdrop is decorative.
Keyboard2.1.1, 2.1.2Tab cycles within the dialog; Esc always closes, so focus is contained but never trapped.
Focus2.4.3, 2.4.7Initial focus by the rules above; focus returns to the trigger on close. 2px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode.
Name, role, value4.1.2<dialog> named by its title (aria-labelledby) and described by its body for confirmations (aria-describedby). Destructive confirmations use role="alertdialog".
Announcements4.1.3Opening is announced from the role and name. The result of the action (“Draft deleted”) goes through the live region after close.
Reflow and text spacing1.4.10, 1.4.12Width capped at the viewport minus 32px; height capped with internal scroll; full-screen sheet for lg below 480px.
Error prevention3.3.4Irreversible deletion always asks first and names what will be lost.

Tokens

  • uxp-color-bg-overlay
  • uxp-elevation-4
  • uxp-layer-modal
  • uxp-color-bg-muted
  • uxp-radius-sm
  • uxp-space-6
  • uxp-motion-duration-md

Data

Data table

Compares rows of structured data. Orbit standardizes the layout and behavior; each screen supplies its own column definitions.

Row density
Orbit tokens, sample of 6
Value Actions
uxp-color-action-primarySemantic#0068C8StableView uxp-color-action-primary
uxp-color-border-inputSemantic#8E8E93StableView uxp-color-border-input
uxp-gradient-textSemantic600 / 500 / 600StableView uxp-gradient-text
uxp-motion-duration-mdPrimitive280msStableView uxp-motion-duration-md
uxp-color-control-thumbSemantic#6E6E73BetaView uxp-color-control-thumb
uxp-color-status-neutral-on-tintSemantic#48484DDraftView uxp-color-status-neutral-on-tint

Sort by Token, Tier or Status (announced, and reflected in aria-sort). Select rows to reveal the bulk bar; Deprecate is undoable from its toast. The scroll region is focusable so keyboard users can scroll it.Token, Tier and Status sort, announced and reflected in aria-sort. Selecting rows reveals the bulk bar; Deprecate can be undone from its toast. The scroll region is focusable so keyboard users can scroll it.

Anatomy
Toolbar (density, filters), bulk action bar, scroll region, <table> with a caption, header row, row headers, cells.
Column types
Text: start-aligned, ink-900. Number and code: end-aligned, uxp-text-mono-data (13/400 tabular). Status: Badge. Action: text link at the row end, named with the row (“View token”). Selection: checkbox column, 52px.
Density
Compact 40px, default 52px, comfortable 64px rows, chosen with Segmented control. Heights are cell min-heights; text spacing overrides grow the row.
Responsive
From 768px down, the table scrolls inside a focusable region with a sticky first column. On small screens a screen can opt into a list view, rendered as a list of description lists. Never restyle table cells with display:block; several browsers then drop the table semantics.
Scope
Orbit ships the chrome: toolbar, selection model, sorting, density, responsive behavior. Column definitions live in the screen as a config array.
Row actions
Up to two text actions per row. More than two go in a Menu at the row end, behind a small “More” button named for the row (“More actions for token”). Actions on several rows go in the bulk bar.

When to use

  • People compare values across rows or sort and select.

When not to use

  • There are fewer than four rows or two columns. Use a list.
  • Each item is visual. Use Cards.
  • You need to lay out a page. Tables are for data.

Accessibility checklist

Data table, WCAG 2.1 criteria
CheckImplementation
Contrast1.4.3, 1.4.11Cells ink-900 16.4:1, headers ink-500 on ink-50 4.7:1. View links blue-600 4.0:1, a documented exception. Selected rows sit on uxp-color-bg-selected-subtle, where ink-900 holds at 15.1:1 and row links switch to blue-700 (5.0:1). Badges per Badge.
Keyboard2.1.1Sort buttons, checkboxes, links and the scroll region are all reachable with Tab.
Focus2.4.72px uxp-focus-ring-color (blue-600, 4.0:1) outline at 2px offset on :focus-visible. Outline, not box-shadow, so it survives forced-colors mode. The scroll region has its own ring.
Name, role, value4.1.2, 1.3.1Native <table>, <caption>, scope="col" and scope="row". Sorted column carries aria-sort. Each row checkbox is named “Select token”. Select-all shows mixed when partial.
Announcements4.1.3“Sorted by Token, descending”, “2 rows selected” and bulk results go through the live region.
Reflow and text spacing1.4.10, 1.4.12Two-dimensional data is exempt from reflow, so horizontal scroll inside the region is allowed; the page itself never scrolls sideways.
Error prevention3.3.4Bulk Deprecate is reversible: its toast offers Undo and stays until used or dismissed.

Tokens

  • uxp-text-mono-data
  • uxp-color-bg-muted
  • uxp-color-bg-selected-subtle
  • uxp-color-text-link-on-tint
  • uxp-color-border-subtle
  • uxp-color-action-secondary-text
  • uxp-radius-sm
  • uxp-target-sm

Part 4 of 5

Governance

Orbit has a single owner today. As UXPective moves toward senior contributors running parts of the platform, this model lets contributors propose, build and review components while the system stays consistent.

Roles

Who does what
RoleTodayResponsible for
OwnerShanicaFoundations, token values, final decisions on proposals
ContributorSenior contributors, as they joinProposing, building, documenting and testing components against the acceptance criteria
Accessibility reviewerA contributor who didn’t build the componentSigning off the AA checklist and the test protocol. Authors never certify their own work.
ConsumerAnyone building a UXPective screenUsing the library, reporting gaps, proposing patterns with evidence
Decision rights
ChangeWho decidesReview
Foundation or token valueOwnerOne contributor
New component or variantOwnerOne contributor and the accessibility reviewer
Fix that changes no value or behaviorAny contributorOne other contributor
DocumentationAny contributorOwner reviews afterwards

Contribution process

  1. ProposeOpen a proposal with screenshots of the pattern on three shipped screens, or two plus the one about to ship.
  2. Entry reviewThe proposal is checked against the entry criteria below. If it doesn’t meet them, the pattern stays local and is added to the backlog with what it’s missing.
  3. DraftBuild it in Figma and code at the same time, with matching names, and write the docs from the component template.
  4. Acceptance reviewThe component is checked against the acceptance criteria below, including its accessibility checklist and the test protocol.
  5. ReviewThe owner and one contributor review the design and code; the accessibility reviewer signs off separately.
  6. PublishThe component joins the library as Beta, and contributors are told what it replaces. It becomes Stable after it has run on shipped screens without needing changes.

Contribution criteria

Entry criteria

  1. Is it on at least three shipped screens?
  2. Does it carry behavior that’s easy to get wrong (keyboard, focus, validation, announcements), or at least three real variants?
  3. Is composing it from existing components worse than naming it?

Acceptance criteria

  • Anatomy, variants, sizes and states written
  • “Use it when” and “Don’t use it when” written
  • All seven accessibility rows filled, none deleted
  • Semantic tokens only; the lint finds no raw values or primitives
  • Figma and code names match
  • Test protocol passed on the built component
  • Examples follow the Writing foundation

Component lifecycle

Every component and token carries one of four states, shown as a badge on its docs page.

Lifecycle states
StateMeansCan screens use it?
DraftBeing built against the acceptance criteriaNo
BetaMeets the acceptance criteria, not yet proven on shipped screens; details may changeYes, with the owner informed
StableProven on shipped screens; changes go through reviewYes
DeprecatedBeing replaced; points to its replacementNo new uses

Deprecation is never a surprise. A deprecated component or token stays available for at least 90 days, shows a banner on its docs page, logs a warning in code, and names exactly what replaces it. It is removed only once no shipped screen uses it.

Component backlog

Patterns that have been proposed but don’t meet the entry criteria yet, with what each one needs.

Backlog
PatternCovered today byNeeded to proceed
Date pickerThe native date input (a Text field configuration)A range-selection use case on a shipped screen
PopoverTooltip for text, Menu for actionsInteractive content that has to float and isn’t a list of actions
StepperOne page per step, each with its own URLA multi-step flow where people need to see every step at once
BreadcrumbSide nav and back linksA surface more than two levels deep

Adoption metrics

These signals show whether screens are built from Orbit or drifting back to one-off decisions.

Adoption signals
SignalMeasured by
Token coverageShare of color, space and type values in code and Figma that resolve to a token, from the lint and Figma’s detached-style count
One-offsLocal components living outside the library
Detached instancesFigma instances detached from the library
Checklist coverageComponents with all seven accessibility rows complete and the protocol passed (22 of 22)
Time to compose a screenOne day for a screen built from existing components

Part 5 of 5

Adoption and impact

How Orbit moved from a library to the default way UXPective screens are built.

Adoption

Docs that mirror the tokens

The internal docs page reads in the same order as Part 4. Writing it is what surfaces the holes: the moment a usage rule can’t be described cleanly, the component isn’t done.

Figma that mirrors code

Same component, prop and variant names. They drift the moment a Figma-only variant appears for one mock, so variants follow the contribution process in both places.

Screen-by-screen migration

Screens moved onto Orbit one at a time, starting with the hardest to maintain. The slowest were the ones with the most one-off layout.

Impact

A new screen takes a day, down from about five. “A day” means a screen composed from existing components with new copy and imagery; a screen with a new interactive module still takes most of the week, and that’s the right number. There is one blue, one text field, and no wrong version of the gradient.

Accessibility moved into the foundations. Every component has an accessibility checklist, every contrast pair is calculated from the tokens, and the few colors that are lighter than AA on purpose are listed as documented exceptions rather than left implicit.

New component ideas still come up on most screens. The entry criteria keep the library small, and proposals that don’t meet them go to the backlog.

Learnings

Accessibility belongs in the foundations

Focus, contrast and announcements touch every component. Built into tokens and system behavior, every component inherits them instead of each one being retrofitted.

Keep the icon set small

I started with 60 icons and 22 were used. The rest made the set harder to scan, so the library now holds 36.

Usage rules before components

Cards and modals get used where they don’t belong unless “when not to use” is written first, so it is part of the acceptance criteria.

Calculate contrast, don’t type it

Typed ratios drift from the tokens they describe. Every ratio on this page is calculated when it’s built, and exceptions are listed explicitly.

Credits

Author
Shanica Roy
Typefaces
Satoshi, Inter and JetBrains Mono
Written
October 2026
Accessibility
WCAG 2.1 AA for text, keyboard and focus, with documented color exceptions