Relay design system

Components

Every primitive in components/ui, drawn from one size contract in components/ui/sizing.ts. Hover, active and focus are forced here through a forceState prop that exists only for this page — app code never sets it.

The inventory

Fourteen components. The props are the Figma variant properties, one for one, and the file is the description each Figma component carries.

Every component, its file, its props and its states
ComponentPropsStatesFile
Button
variant
primary · secondary · ghost
size
sm · md · lg
icon
slot (optional)
loading
boolean
default · hover · active · focus-visible · disabled · loadingcomponents/ui/button.tsx
IconButton
variant
ghost · secondary
size
sm · md · lg
default · hover · active · focus-visible · disabledcomponents/ui/icon-button.tsx
Input
size
sm · md · lg
invalid
boolean
default · hover · focus-visible · disabled · invalidcomponents/ui/input.tsx
Select
size
sm · md · lg
default · hover · focus-visible · disabledcomponents/ui/select.tsx
Chip
tier
now · next · later · fyi · success · neutral
size
sm · md
staticcomponents/ui/chip.tsx
TimePill
dueAt
ISO time (the pill reads late, due in, due at or tomorrow from it)
tier
now · next · later · fyi
size
sm · md
staticcomponents/relay/time-pill.tsx
TierIcon
tier
now · next · later · fyi
safety
boolean (now only)
staticcomponents/relay/tier-icon.tsx
KbdNonestaticcomponents/ui/kbd.tsx
Panel
tone
card · inner · raised · overlay
radius
r-4 · r-5
staticcomponents/ui/panel.tsx
SectionBand
icon
slot
label
text
count
number
staticcomponents/ui/section-band.tsx
Toast
message
text
detail
text (optional)
action
slot
static · entering · leavingcomponents/ui/toast.tsx
SkeletonNonestatic — no shimmercomponents/ui/skeleton.tsx
EmptyState
icon
slot
title
text
description
text
action
slot
staticcomponents/ui/empty-state.tsx
ErrorState
title
text
description
text
action
slot
staticcomponents/ui/error-state.tsx

The size contract

Four heights, and height decides everything else: radius is a function of height (r = round(h × 0.28)), as is the icon size and the horizontal padding. Anything sharing a row shares one entry — no 32 beside 40, ever.

Control sizes, their radius, and what each is for
StepHeightRadiusWhere
xs288Chips, time pills, controls inside a row's meta cluster
sm328Secondary actions inside dense surfaces
md4010The default: inputs, selects, most buttons
lg4812The primary action

…

…

…

Button

The primary button is filled with --text-1, not the accent. The accent is a focus ring and a link, and nothing else — a saturated fill inside a tinted card makes the button louder than the tier it sits in, which is the one thing this product’s ranking cannot survive.

Button variant
Button variantDefaultHoverActiveFocus visibleDisabledLoading
Primary
Secondary
Ghost

Icon button

Icon button variant
Icon button variantDefaultHoverActiveFocus visibleDisabled
Ghost
Secondary

Input and select

Input
InputDefaultHoverFocus visibleDisabledError
Text
Select
SelectDefaultHoverFocus visibleDisabled
Carrier

Chip, time pill, tier icon

A chip carries a tier’s text and border, never a saturated fill. Shape carries the tier too — filled octagon, filled triangle, hollow circle, hollow square — so the ranking survives having the hue taken out.

Act nowUp nextWhen you canFor your infoDoneOn breakDue in 50 min2 h late

Surfaces

A card is one hairline plus one shadow step. In dark, alpha-white borders compound, so a bordered card never contains another — the inner box steps the surface instead.

Panel · card

Panel · inner, for a box inside a card

Panel · overlay

Popovers, menus, the palette, the toast

Act now1

Lithium battery orders missing hazmat labels

12 orders cannot ship legally

Up next3

Feedback and absence

Done: Label printer offline at Pack 7

Next: Lithium battery orders missing hazmat labels

Open search⌘K

Nothing waiting on you

When something needs a decision, it shows up here first.

Skeleton — no shimmer, ever

Tier icon and time pill

Shape carries the tier before colour does: octagon, triangle, circle, square. The pill never chooses its own colour from lateness. Two hours late in Up next is Up next amber.

Tier icon
Tier iconAct nowUp nextWhen you canFor your info
Plain
Safety
Act now only
Act now only
Act now only
Time pill
Time pillAct nowUp nextWhen you can
Late, minutes
Late by 30 min
Late by 30 min
Late by 30 min
Late, hours
Late by 3 h
Late by 3 h
Late by 3 h
Due in
Due in 50 min
Due in 50 min
Due in 50 min
Due at
Due in 240 min
Due in 240 min
Due in 240 min
Tomorrow
Tomorrow 10:40
Tomorrow 10:40
Tomorrow 10:40
Done at 10:12Success is the only other hue a chip may carry.

Anatomy

What each part is made of, read off the running component. A number here is a number the browser reports, not one typed into a document.

Buttoncomponents/ui/button.tsx
  1. 1Container. 48 high at lg, radius 12, 20 of padding. Filled with text-1, never the accent.
  2. 2Icon. 20px at lg, 1.75 stroke. Optional, and it sits before the label.
  3. 3Label. 16 on 24, weight 500. Verb first, one to three words.
  4. 4Focus ring. 2px outline at 2px offset, the focus token. It is there on the frame the key lands.
Time pillcomponents/relay/time-pill.tsx · components/ui/chip.tsx
Late by 2 h
  1. 1Container. 28 high, radius 8, a 1px border in the tier's line colour.
  2. 2Icon. 16px clock. Always the clock, whatever the tier.
  3. 3Label. 14 on 20, tabular figures, so a minute ticking over does not move the box.
  4. 4Tint. The tier's tint. Text and border carry the hue; the fill is the quietest step.
Inputcomponents/ui/input.tsx
  1. 1Field. 40 high at md, radius 10. 48 on a handheld, so the target is real.
  2. 2Placeholder. text-3. It is a hint, never the only label.
  3. 3Border. 1px line-1. Invalid uses the Act-now line: an error is the one hue an input may carry.
Toastcomponents/ui/toast.tsx

Marked done

Label printer offline

  1. 1Shell. 360 by 56, radius 16, the overlay surface and the third shadow step. Bottom-left.
  2. 2Message. 14 on 20, text-1. Says what happened, past tense.
  3. 3Detail. 14 on 20, text-2. Says what changed as a result.
  4. 4Action. A 44px target, because a control that lives for eight seconds cannot be hard to hit.

The same components in wire

Not a filter, and not a second set of components: the chroma multiplier goes to zero, radii collapse, depth turns off, and the sans becomes the mono. Everything below is the same code as above.

Act nowUp nextWhen you can