Foundations

The decisions everything else inherits — components reference only the semantic layer, which is what makes a theme switch a token remap.

Color #

Two layers: raw ramps hold the palette, semantic aliases name a job and re-point per mode. Components reference only the aliases, which is what makes dark mode a token remap.

Never reference a ramp step from a component — it cannot know which mode it is in.

The same aliases, both themes, at once
light pinned — ignores the page toggle
canvas
surface
raised
subtle
muted
inverse
Cohort 12-B · interim analysis text-primary
Screening complete, 1,204 subjects enrolled. text-secondary
Last synced 08:41 UTC text-tertiary
Export locked until sign-off text-disabled
Run analysis Cancel
SuccessWarningDangerInfo
dark pinned — ignores the page toggle
canvas
surface
raised
subtle
muted
inverse
Cohort 12-B · interim analysis text-primary
Screening complete, 1,204 subjects enrolled. text-secondary
Last synced 08:41 UTC text-tertiary
Export locked until sign-off text-disabled
Run analysis Cancel
SuccessWarningDangerInfo
Layer 1 — raw scales
Gray --gray-* Every surface, border and text colour in both modes.
  • 0
  • 25
  • 50
  • 100
  • 200
  • 250
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 850
  • 900
  • 925
  • 950
  • 975
Blue --blue-* Primary action in light mode. Links in both modes.
  • 50
  • 100
  • 200
  • 300
  • 400
  • 450 ·
  • 500
  • 600
  • 700
  • 750 ·
  • 800
  • 900
  • 950
Lime --lime-* Primary action in dark mode. Never used in light.
  • 50
  • 100
  • 200
  • 300
  • 350 ·
  • 400
  • 450 ·
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
Green --green-* Success. Run completed, sample passed QC, changes saved.
  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
Amber --amber-* Warning. Recoverable, but the user should look.
  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
Red --red-* Danger. Destructive actions and blocking errors.
  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
Sky --sky-* Informational. A separate hue from blue so an info banner never reads as clickable.
  • 50
  • 100
  • 200
  • 300
  • 400
  • 500
  • 600
  • 700
  • 800
  • 900
  • 950
Layer 2 — semantic aliases
Alias Job Light Dark
--bg-canvas The page itself. Nothing sits behind it. --gray-25 --gray-975
--bg-surface The default panel: cards, tables, every preview frame on this page. --gray-0 --gray-950
--bg-surface-raised A surface floating above another surface — menus, popovers, modals. --gray-0 --gray-900
--bg-subtle Recessed fill: hovered rows, code blocks, table headers. --gray-100 --gray-925
--bg-muted One step further down: disabled fills, slider and progress tracks. --gray-250 --gray-900
--bg-inverse A block deliberately opposite the page — tooltips, inverse callouts. --gray-950 --gray-50
--border-subtle Divider inside a component, where structure rather than separation is the point. --gray-100 --gray-900
--border-default The standard hairline around a surface. --gray-200 --gray-850
--border-strong A border that has to be seen: input outlines, ghost buttons. --gray-300 --gray-800
--text-primary Headings and anything carrying the main message. --gray-950 --gray-50
--text-secondary Body copy and supporting prose. --gray-700 --gray-400
--text-tertiary Labels, captions, metadata. Present, not competing. --gray-600 --gray-500
--text-disabled Text in a control the user cannot act on. Below AA on purpose — WCAG exempts inactive controls, and the low contrast is the signal. --gray-400 --gray-600
--text-on-primary Text and icons sitting on an accent fill. --gray-0 --gray-975
--text-on-inverse Text sitting on --bg-inverse. --gray-50 --gray-950
--text-link Inline links. Stays blue in dark mode so lime is reserved for actions. --blue-600 --blue-450
--accent Primary action fill. The one colour a screen is allowed to compete for. --blue-600 --lime-400
--accent-hover Accent under the pointer. --blue-700 --lime-350
--accent-press Accent while the pointer is down. --blue-750 --lime-450
--accent-fg Label on the accent fill. Flips to near-black in dark because lime is a bright fill. --gray-0 --gray-975
--accent-muted Low-alpha accent wash: text selection, active nav item, tinted focus fill. rgba(37, 99, 235, 0.1) rgba(163, 230, 53, 0.14)
--accent-glow Outer bloom on a hovered primary button. rgba(37, 99, 235, 0.3) rgba(163, 230, 53, 0.35)
--overlay Scrim behind a modal or drawer. rgba(22, 24, 29, 0.5) rgba(0, 0, 0, 0.65)
--focus-ring The 2px focus outline. Points at --accent so no component invents its own focus colour. --accent --accent
Status families
Success --success-*

QC passed · 384/384 wells

Warning --warning-*

Plate 7 flagged for re-read

Danger --danger-*

Import failed · 12 rows rejected

Info --info-*

Cohort definition updated 2h ago

Role What it is for
-solid Fill for a filled control — a destructive button, a status dot.
-fg Text and icons that sit on that fill.
-bg Tint behind a banner, badge or table row.
-border The hairline that pairs with the tint.
-text The status colour as text on the page canvas. AA-checked.
Do

Reach for the alias that describes the job; if none fits, the gap is in theme.css and belongs there.

Don't

Don't hard-code a hex, a ramp step, or a [data-theme='dark'] override in a component.

Typography #

One sans family for what a person reads, one mono for what a machine produced. Eight sizes, three weights, and a 10px root so 1rem = 10px throughout.

Do not add a size between two steps because one heading is two pixels too tall in one layout.

Scale
h1
A design system you can click
h2
Cohort builder
h3
Eligibility criteria
h4
Inclusion rules
h5
Biomarker thresholds
h6
Assay reference
Text roles
body-lg

A lede sits directly under a title and carries the one sentence a reader will take away if they read nothing else.

body

Default body copy at 15px with a 1.6 line-height. Long-form paragraphs are capped around 68 characters, because a line longer than that loses the reader on the return sweep.

small

Secondary copy: helper text under a field, a table caption, the second line of a list row.

caption

Assay: IL-6 ELISA · Plate 07 · Read 450nm

label Cohort name
overline Foundations
code Sample SPL-1204-B7 failed the min_reads threshold.
link See the colour foundations for how link colour is chosen per mode.
Truncation
3-line clamp

Cohort 12-B was defined from the screening registry on 14 March and narrowed by two inclusion rules: an eGFR above 45 and at least one IL-6 reading above the assay's reference range within the previous ninety days. Subjects with an active oncology diagnosis were excluded at intake rather than filtered afterwards, which is why the enrolled count differs from the screened count by more than the withdrawal log explains.

Clamps at three lines, then ellipses. Full text stays in the DOM.
Token Value Computed Line-height Used for
--fs-h1 clamp(4rem, 3rem + 3vw, 6.4rem) 40 → 64px 1.1 Page title. One per page.
--fs-h2 clamp(2.8rem, 2.24rem + 1.4vw, 4rem) 28 → 40px 1.1 Section titles.
--fs-h3 2.4rem 24px 1.1 Sub-section titles.
--fs-h4 2rem 20px 1.1 Card titles, stat values.
--fs-body-lg 1.7rem 17px 1.6 Lede paragraph. Also the size h5 renders at.
--fs-body 1.5rem 15px 1.6 Default body copy. Also the size h6 renders at.
--fs-small 1.3rem 13px 1.55 Buttons, table cells, secondary copy.
--fs-micro 1.2rem 12px 1.4 Overlines, column headers, captions.
Property Value Notes
Family — UI and prose --font-sans Geist, then the system stack.
Family — values --font-mono Geist Mono. Token names, IDs, counts, code.
Weight — body 400 Paragraphs and table cells.
Weight — labels 500 Form labels, buttons, nav, active states.
Weight — headings 600 h1–h6. There is no 700 in the system.
Tracking — headings --track-tight (-0.02em) Large type sets loose by default; this pulls it back.
Tracking — mono caps --track-mono (0.08em) Uppercase mono is unreadable without it.
Do

Pick the step by role, and change weight or colour before reaching for a larger size.

Don't

Don't set a bare px font-size — it drifts out of sync the moment the scale is retuned.

Spacing & grid #

A 4px base unit with ten steps — linear at the small end where the difference is visible, geometric at the large end where it is not. Layout sits on a 1120px container and a 12-column grid.

If a layout needs --space-9 between two related controls, the problem is the grouping, not the spacing.

Scale
  • --space-1 4px Icon-to-label inside a dense control.
  • --space-2 8px Label to field. Gap inside a button.
  • --space-3 12px Table cell padding. Chip padding.
  • --space-4 16px Padding inside a small card. Gap in a row of controls.
  • --space-5 24px Grid gutter. Padding inside a standard card.
  • --space-6 32px Preview stage padding. Gap between related blocks.
  • --space-7 48px Space between documented sections.
  • --space-8 64px Column gap in the page layout.
  • --space-9 96px Major break between unrelated content.
  • --space-10 128px Top-level page rhythm.
12-column grid
123456789101112
span 6 span 6
span 8 span 4
3 3 3 3
Layout token Value Job
--container 1120px Max content width. Centred, with gutters outside it.
--container-pad 24px Horizontal gutter at every viewport.
--section-pad clamp(64px, 10vh, 128px) Vertical rhythm between page sections.
--header-h 64px Fixed header height. Anchor offsets are derived from it.
Breakpoint Name What changes
(min-width: 640px) sm Do/don’t guidance goes two-up. State matrices gain a column. Side-by-side theme panels split.
(min-width: 1024px) lg The mobile bar is replaced by the fixed header, and this page becomes a sticky TOC rail plus content column.
Do

Compose padding and gaps from the scale so vertical rhythm survives contact with real content.

Don't

Don't split the difference with a 10px or 18px value; one is invisible, twenty read as unfinished.

Elevation #

Three levels, --elevation-1/2/3, for anything that has to read as floating above the page — cards, menus, modals, toasts.

Elevation is not hierarchy: a card is higher because something can appear underneath it.

The same three levels, both themes
light
--elevation-1 Resting card, input, badge.
--elevation-2 Dropdown, popover, hovered card.
--elevation-3 Modal, drawer, toast.
dark
--elevation-1 Resting card, input, badge.
--elevation-2 Dropdown, popover, hovered card.
--elevation-3 Modal, drawer, toast.
Token Light resolves to Dark resolves to Used for
--elevation-1 shadow-sm only hairline + shadow-sm Resting card, input, badge.
--elevation-2 shadow-md only hairline + shadow-md Dropdown, popover, hovered card.
--elevation-3 shadow-lg only strong hairline + shadow-lg Modal, drawer, toast.
Do

Use --elevation-* and pair each level with its surface — a popover is --bg-surface-raised at --elevation-2.

Don't

Don't reach for --shadow-lg on a dark surface; a shadow on near-black is invisible by construction.

Radius #

Four values. Radius is how a component announces its size class: the smaller the element, the tighter the corner, so a chip and a modal do not share a curve.

Do not mix radii inside one component — a card and its button at the same radius read as two cards.

Scale
--radius-sm 6px
--radius-md 8px
--radius-lg 12px
--radius-full 999px
Token Value Applied to
--radius-sm 6px Buttons, inputs, selects, inline code, TOC links.
--radius-md 8px Guidance boxes, nested panels, menu items, alerts.
--radius-lg 12px Cards, preview frames, modals, drawers.
--radius-full 999px Pills, badges, avatars, switch tracks, jump-bar chips.

Iconography #

Inline 24×24 SVGs on a 24-unit grid, stroked at 1.5 with round caps, no fills, in three sizes. Inlined rather than loaded as a font, so every icon inherits colour.

An icon on its own is not a label: icon-only controls need an aria-label, decorative ones need aria-hidden.

Sizes
--icon-sm 16px
Inside dense controls: chips, table cells, small buttons.
--icon-md 20px
Default. Buttons, nav items, form affordances.
--icon-lg 24px
Standalone: empty states, section markers, status headers.
Colour
Inherits text
Search cohorts
success
Run completed
warning
Needs review
danger
Import failed
info
Definition changed
Do

Keep the 24×24 viewBox at every size, and name the action rather than the picture — 'Export cohort', not 'Download icon'.

Don't

Don't colour an icon to decorate it, and don't ship an icon-only control without a label.

Motion #

Four durations and three easing curves. Motion confirms a change — a menu opening, a theme flipping — and never carries information on its own.

Nothing waits on an animation; if the interface is unusable until it finishes, that is a loading state.

Durations — same easing, four speeds
  • --dur-fast 150ms Colour changes on hover and focus.
  • --dur-base 200ms Theme switch, card lift, border transitions.
  • --dur-reveal 400ms Scroll-in reveals, overlay entrances.
  • --dur-slow 600ms Large travel: drawers, full-height panels.
Easings — same duration, three curves
  • --ease-out cubic-bezier(0.25, 0.46, 0.45, 0.94) The default. Anything responding to a click or hover.
  • --ease-out-expo cubic-bezier(0.16, 1, 0.3, 1) Entrances. Fast off the line, long settle — reads as confident rather than slow.
  • --ease-in-out cubic-bezier(0.4, 0, 0.2, 1) Symmetric movement: something that travels and comes back.
Token Value Used for
--dur-fast 150ms Colour changes on hover and focus.
--dur-base 200ms Theme switch, card lift, border transitions.
--dur-reveal 400ms Scroll-in reveals, overlay entrances.
--dur-slow 600ms Large travel: drawers, full-height panels.
--ease-out cubic-bezier(0.25, 0.46, 0.45, 0.94) The default. Anything responding to a click or hover.
--ease-out-expo cubic-bezier(0.16, 1, 0.3, 1) Entrances. Fast off the line, long settle — reads as confident rather than slow.
--ease-in-out cubic-bezier(0.4, 0, 0.2, 1) Symmetric movement: something that travels and comes back.
Do

Transition named properties with a duration and an easing token; anything user-triggered stays at --dur-fast or --dur-base.

Don't

Don't write `transition: all`, and don't animate width, height or top/left when transform will do.

Z-index #

Eight named layers in one place, spaced 100 apart so a value can be slotted between two without renumbering. Everything that overlaps draws its stacking order from here.

The bare z-index that fixes it today is what traps a tooltip behind a modal six months from now.

Stacking order
  • --z-base 0 Page content
  • --z-raised 10 Sticky table header
  • --z-dropdown 100 Menus, popovers
  • --z-sticky 200 Site header, jump bar
  • --z-drawer 300 Drawer and scrim
  • --z-modal 400 Modal and scrim
  • --z-toast 500 Toasts — clear modals
  • --z-tooltip 600 Tooltips — always top
Do

Give every new overlay one of the eight names, adding to the scale in theme.css if a layer is genuinely missing.

Don't

Don't create a stacking context — transform, filter, opacity below 1 — on an ancestor of an overlay.

Actions

Everything that commits a user to something, governed by one rule: a single primary action per view.

Buttons #

Commits the user to something: submitting a form, exporting a file, destroying a record. One primary per view; everything else steps down to secondary or tertiary.

Not for navigation — if it changes the URL it is a link, and assistive technology announces the two differently.

Variants
Variant × size
SmallMediumLarge Primary Secondary Tertiary Success Danger
With icons
States
Default
Hover
Focus visible
Pressed
Disabled
Loading — click to run
Ready
Do

Give the primary action the most specific verb available — 'Export cohort', not 'Submit'.

Don't

Don't ship two primary buttons in one view, or use the danger variant for anything reversible.

Icon button #

A button whose only label is an icon, for dense toolbars, table rows and card headers. Only for actions with a settled visual convention: close, search, download, delete.

The accessible name is not optional, and any icon whose meaning has to be guessed needs a visible tooltip too.

Sizes
Variants
Named by tooltip, or by aria-label alone
States
Default
Hover
Focus visible
Pressed
Disabled
Loading
Do

Keep the tooltip text and the aria-label byte-identical, and give every icon button one.

Don't

Don't ship the 32px size on a touch surface — it is below the 44px minimum pointer target.

Button group #

Two or more related controls presented as one unit. The connected form keeps separate actions on one object together; the segmented form is a single exclusive choice between views.

A segmented control is not a tab set or a filter — past five options, or one wrapping label, it should be a select.

Connected group
Segmented control — click, or Tab in and use the arrow keys

Result density: Compact

States
Default
Hover
Focus visible
Disabled option
Group: hover
Group: focus
Do

Label the group itself with role='group' or role='radiogroup' and an aria-label.

Don't

Don't let a segmented control run an action, and never put a destructive button in a connected group.

Forms & inputs

The densest surface in most clinical and scientific tools, and the one where state coverage matters most.

Text input #

The default control for a short value the user types rather than picks — a sample ID, participant initials, a site code. For answers that are open-ended but short.

Not for a value drawn from a known set, anything past one line, or a number people will nudge.

States
Default

Format: SPL-YYYY-NNNN.

Hover

Format: SPL-YYYY-NNNN.

Focus

Format: SPL-YYYY-NNNN.

Filled

Format: SPL-YYYY-NNNN.

Disabled

Locked after accessioning.

Error

Sample ID must match SPL-YYYY-NNNN.

Affixes, clear button, counter

A leading icon is decoration — the label still carries the meaning.

The unit is repeated in the label text for screen readers: volume in millilitres.

The clear button appears only when there is something to clear.

Printed on the 25 mm cryo label.

0 / 32
Sizes
Size Height Type Where
.ds-input--sm 32px --fs-small Inline table filters, toolbars, dense grids
.ds-input 40px --fs-body Everything else. If you are unsure, this one.
.ds-input--lg 48px --fs-body-lg Single-field steps and touch-first flows
Do

Say what the format is before the user types it, as helper text rather than as an error.

Don't

Don't use the placeholder as the label — it vanishes on the first keystroke.

Textarea #

Multi-line free text where the length is genuinely unpredictable: protocol deviation notes, reviewer comments, the reason a sample was rejected.

Not for structured content needing per-line validation, and not a dumping ground for data you will later parse.

States
Default

Visible to the monitor at the next visit.

Hover

Visible to the monitor at the next visit.

Focus

Visible to the monitor at the next visit.

Filled

Visible to the monitor at the next visit.

Disabled

Read-only once the deviation is signed.

Error

A deviation note needs at least 20 characters.

Auto-resize — type into it

Grows to 260px, then scrolls rather than pushing the page around.

0 / 280
Do

Let the field grow with the content up to a sensible cap, then hand overflow back to a scrollbar.

Don't

Don't cap the height at three lines and remove the resize handle.

Select #

One value from a short, known, mutually exclusive list — specimen type, cohort, review outcome. Best up to about a dozen options, where people recognise rather than recall.

Past roughly fifteen options use a combobox; for two to four, radios show every choice without a click.

Live — click, or focus and use the arrow keys
Specimen type

Must match the specimen on the accession form.

States
Closed
Specimen type
Plasma (EDTA)

Set at accessioning.

Hover
Specimen type
Plasma (EDTA)

Set at accessioning.

Focus
Specimen type
Plasma (EDTA)

Set at accessioning.

No selection
Specimen type
Select specimen type

Set at accessioning.

Disabled
Specimen type
Plasma (EDTA)

Set at accessioning.

Error
Specimen type
Select specimen type

Specimen type is required before accessioning.

Open — option states
Do

Keep the options in a stable, meaningful order and explain any disabled option in place.

Don't

Don't hide the current value behind a placeholder that reads like a choice.

Combobox #

A text input backed by a filtered list: type a few characters to cut hundreds of options down to one. The multi-select variant is the right control for 'any number of sites'.

Not for a list short enough to scan at a glance, and not when arbitrary text is genuinely allowed.

Single select — type to filter

Turnaround time is shown on the right of each option.

Multi select — chips removable by click or Backspace
Site 012 — Brightmoor General Site 019 — Lakeside Oncology

Backspace on an empty field highlights the last site; press it again to remove it.

Do

Keep the query and the list in sync, announce the result count, and make Escape a reliable way back.

Don't

Don't silently discard a half-typed query on blur — it reads as a bug and loses the user's work.

Search input #

Query-as-you-type over a set the user is scanning — samples, participants, files. It filters what is already on screen; it does not submit.

Not for a global search that navigates to a results page, and never the only route to content.

Idle & focused
Idle
Focused
With results & no results — both live, edit either one
With results
No results
Do

Write a no-results state that repeats what was searched for and offers the next move.

Don't

Don't bury search behind an icon-only expander on a screen where searching is the main job.

Password input #

Credential entry — masked by default, with a show/hide toggle so people can check what they typed before they commit to it.

Not a general-purpose secret field for API keys or tokens the user is meant to copy back out.

Live — the toggle is real

At least 12 characters. Paste is allowed and encouraged.

States
Default

At least 12 characters.

Filled — masked

At least 12 characters.

Focus

At least 12 characters.

Revealed

At least 12 characters.

Disabled

At least 12 characters.

Error

Use at least 12 characters.

Do

Allow paste, state the rules before the field, and keep the reveal available right up to submit.

Don't

Don't block paste or silently truncate at a maximum length; both push people toward weaker passwords.

Date picker #

A date the user picks in relation to other dates — a collection date inside a study window, a monitoring window spanning two months. The calendar is what answers 'which Monday'.

Not for a date the user already knows exactly, like a date of birth: a typed field beats sixty clicks.

Single, range, and out of range — all live
Single date
Collection date

Must fall inside the study window (16 Feb – 10 Apr 2026).

Date range
Monitoring window

Pick the first day, then the last. Picking earlier than the start restarts the range.

Out of range
Collection date

Study window: 16 Feb – 10 Apr 2026.

Do

Constrain to the valid window and keep out-of-range days visible but unselectable.

Don't

Don't leave range validation until submit, two screens after the mistake was made.

File upload #

Bringing a file in from the user's machine — assay exports, sample manifests. Drag-and-drop plus a real file input, because dragging is not available to everyone.

Not for very large or resumable transfers; past a few hundred megabytes this needs a chunked uploader behind it.

Live — drag a file onto it, or choose one

Drop an assay export here

FCS, CSV or TSV · up to 25 MB

One file at a time. Rejected formats are named in the error, not just refused.

States
Idle

Drop an assay export here

FCS, CSV or TSV · up to 25 MB

Choose file
Drag over

Drop an assay export here

FCS, CSV or TSV · up to 25 MB

Choose file
Uploading
panel-a-run-2026-03-11.fcs
8.4 MB of 13.6 MB · 62%
Success
panel-a-run-2026-03-11.fcs
13.6 MB · uploaded 2 min ago
Error

Drop an assay export here

FCS, CSV or TSV · up to 25 MB

Choose file

run-2026-03-11.zip is not a supported format. Upload FCS, CSV or TSV.

Do

State the accepted formats and the size cap up front, then name the specific reason in the error.

Don't

Don't make drag-and-drop the only way in — keyboard and most touch users cannot drag.

Checkbox #

Independent yes/no choices, or a single opt-in staged until the form is submitted. Reach for it whenever more than one option in a group can be true at once.

Not for a setting that applies the instant it is flipped — that is a switch.

States
Unchecked
Hover
Focus
Checked
Indeterminate
Disabled
Checked · disabled
Error
Helper text and single opt-in
Select all — a real indeterminate parent
Do

Wrap the input inside its <label> so the whole line is a hit target.

Don't

Don't fake 'some selected' with a third visual state; set el.indeterminate so the announcement agrees.

Radio #

One choice from a small, mutually exclusive set — two to five options — where seeing every option at once is worth the vertical space. The card variant adds consequence copy.

Past roughly six options use a select, and never ship a group with nothing preselected unless 'no answer' is legitimate.

States
Unselected
Hover
Focus
Selected
Disabled
Selected · disabled
Group — fieldset, legend, one name
Specimen type

Determines which assays the site can be assigned. One per participant.

Radio card
Data access tier

Each tier changes what leaves the secure enclave. Pick the narrowest one that answers the question.

Do

Wrap every group in a fieldset with a legend and give the inputs one shared name.

Don't

Don't build radio cards from divs with click handlers; keep a real input inside the card.

Toggle / switch #

A binary setting that takes effect immediately, with no Save step. Use it in preference panels, filter bars and row-level controls where the result is visible right away.

If the change only lands when the form is submitted, use a checkbox.

States
Off
On
Hover
Focus
Disabled off
Disabled on
Label placement — settings rows
Compact — control first, live state
Choosing between a switch and a checkbox
Question Switch Checkbox
When does it apply? The moment it is flipped On submit
How is it undone? Flip it back Discard the form
Where does it live? Settings, filter bars, row controls Forms, consent, multi-select lists
Many of them together? Rarely — one setting per row Yes — that is the normal case
Semantics role="switch" + aria-checked Native checked
Do

Label the state the switch controls, not the action — 'Auto-refresh cohort counts'.

Don't

Don't put a switch in a form with a Save button; people will believe the change already landed.

Slider #

Choosing an approximate value inside a known range, where the direction of travel matters more than the exact number. The range variant sets both ends of a band with one control.

Not for a value that has to be exact or recalled from memory — a dose, an ID, a date.

Single handle — drag for the value bubble
35%
35%
Two handles — clamped so they cannot cross
Age at diagnosis 45–72 years
45 72
Step marks and disabled
12 mo
12 mo
60% · locked
Do

Show the value at all times and expose it as text with its unit via aria-valuetext.

Don't

Don't let range handles cross or stack; a filter that can invert silently returns nothing.

Form layout #

The conventions that make a form readable before a single field is styled: label placement, required marking, where helper text and validation go, and how fields are grouped.

Do not mix label placements in one form, mark both required and optional, or move a field's meaning into placeholder text.

Label placement
Top — the default

One scan column, no width guessing, survives translation.

Inline — dense panels only

Short, familiar, repeated labels. Collapses to top below 640px.

Form conventions and how each one is implemented
Convention Rule Implementation
Required field Marked when most fields are optional required on the input, plus an aria-hidden asterisk in the label
Optional field Marked when most fields are required. Never both in one form. The word "Optional" after the label text in .ds-optional
Helper text Below the control, always visible, never a placeholder .ds-help wired with aria-describedby
Error Replaces nothing — sits under the helper text, on blur not on keystroke aria-invalid="true" plus .ds-error in aria-describedby
Success Only where confirmation is worth the noise — availability, format, sync .ds-success with an icon, not colour alone
Grouping Any set of controls answering one question <fieldset> + <legend>
Inline validation — blur the fields to run it

Used for protocol amendments and query resolution.

Enter a valid email address — for example coordinator@site.org.

Two-letter country code, dash, three digits — for example GB-104.

Identifier is free — not yet claimed by another site.

Assembled — clinical site activation intake

All protocol amendments are sent here.

Specimen types this site can provide

Select every type the site is equipped to collect.

Consent version on file
Demo form — nothing is sent.
Do

Put labels above the field: one scan column, and no reflow when a translation runs longer.

Don't

Don't hold validation until submit, and never signal it with colour alone.

Feedback & status

What the interface says back — loading, empty, and error, specified here rather than improvised per screen.

Toast #

A short, transient confirmation that something finished in the background — an export queued, a sync completed. It never blocks the screen and never needs a response.

Not for anything the user must act on, must read in full, or might need again five minutes from now.

Spawn a toast — bottom right of the viewport

Hover or tab into a toast to freeze its countdown

Anatomy — the four variants, rendered in flow

Sync started

Pulling assay results for study ONC-2291 from the connected LIMS.

Sample 8842 removed from cohort

It stays in the study and can be added back at any time.

3 wells excluded

Plate P-118 wells B4, C7 and D2 fell below the QC threshold.

Sample QC run failed

Run QC-4417 stopped at well 12 of 48. No results were written.

Do

Keep a toast to one sentence of consequence and, at most, one action.

Don't

Don't stack toasts as a status surface; three at once means the job needs a progress indicator.

      Alert / banner #

      A message that stays on the page until the condition behind it changes. Inline alerts speak for their section; page-level banners speak for the whole view.

      Not for confirming an action just taken, and anything permanent belongs in the page copy instead.

      Inline — the four semantics

      Results are provisional until the batch is signed off by the study lead.

      Cohort ONC-2291 saved. 1,284 samples are locked for analysis.

      3 of 48 wells failed QC

      Plate P-118 wells B4, C7 and D2 fell below the intensity threshold. They are excluded from the summary statistics until they are re-run or overridden.

      Page level — full bleed under the app header
      Study ONC-2291 · Samples Read only

      This study is locked for the interim analysis. You can read and export, but edits are disabled until 12 August.

      Sample table

      Do

      Say what changed, what it affects, and what the user can do about it.

      Don't

      Don't make an alert dismissible while the condition it reports is still true.

      Badge / tag / chip #

      A compact label attached to something else. Status badges report state, count badges carry a number, and chips represent a user-set value — which is why only chips are removable.

      A badge is not a button, and it cannot carry a sentence: past three words it belongs in a column.

      Status — icon and label, never colour alone
      Running Passed QC Flagged Failed Draft
      Dot indicator — for dense rows where a glyph is too loud
      Running Passed QC Flagged Failed Draft
      Count
      12 3 148 99+ Failed wells 3
      Chips — click the ✕, they really go
      Site 04 — Leiden Assay: RNA-seq Collected after 2025-01-01 QC: passed
      Do

      Give every status badge its own glyph and keep that pairing constant across the product.

      Don't

      Don't invent a sixth status colour; a sixth meaning belongs in a column, not in a hue.

      Progress #

      For work with a duration the user is waiting on. Determinate when you know how much is left; indeterminate when you genuinely do not, and only then.

      Below about one second a progress bar reads as a glitch, and a fake bar that stalls at 90% destroys trust.

      Determinate — press run, the bar and aria-valuenow both move
      Exporting cohort ONC-2291 0%
      Sizes and tone
      Uploading plate map P-118 68%
      Storage used — 41 of 50 GB 82%
      Indeterminate — no aria-valuenow
      Preparing export — counting eligible samples
      Do

      Put the item count in the label — 'Processing 312 of 1,284 samples' beats a percentage.

      Don't

      Don't leave a determinate bar sitting at 100% after the work is done.

      Spinner #

      The smallest possible 'working on it', for waits too short or too unpredictable to measure. Inline next to the thing it belongs to, or centred in the region it fills.

      Over a couple of seconds a skeleton says more, and anything measurable wants a progress bar.

      Inline — sized to the text it sits beside
      Loading sample manifest…

      16px ring against 15px body text — a spinner larger than its label reads as an error icon

      Section level
      Sample results
      Fetching 1,284 sample records
      Do

      Delay a spinner by roughly 200–300ms; most requests resolve inside that window.

      Don't

      Don't replace an entire populated view with one centred spinner on refresh.

      Skeleton #

      A grey cast of the layout that is loading, for when you know the shape of what is coming. It holds the page geometry still so nothing jumps when the data lands.

      Only when the skeleton actually matches the result — never skeleton an unknown shape.

      Card
      Loading sample card
      Table rows
      Sample Assay QC
      Loading sample table
      Text block
      Loading study summary
      Do

      Match the skeleton to the real layout so the content lands without a single pixel of shift.

      Don't

      Don't shimmer under reduced motion; the static blocks still hold the layout, which was the job.

      Empty state #

      What a collection looks like with nothing in it. Two variants, not interchangeable: a first run that teaches and offers a way in, and a filtered result that explains and offers a way back.

      Do not use an empty state to apologise — zero results is a normal outcome, not a failure.

      Nothing yet — teach, then offer the first action

      No cohorts yet

      A cohort is a saved set of samples you can analyse and export as a unit. Build one from the sample table, or import a list you already have.

      Filtered to nothing — name the filters, offer the way back
      Samples · study ONC-2291 0

      No samples match these filters

      Nothing in this study is both an RNA-seq assay and collected after 2025-01-01. The study itself has 1,284 samples.

      Do

      Make the first-run state carry the onboarding; it is the most-read screen in a new account.

      Don't

      Don't ship a centred grey 'No data' and call it done.

      Error state #

      A region or route that could not render — a request that timed out, a service that is down, a permission revoked mid-session. Always paired with a way to try again.

      This is not a form-field error; using it for a validation failure hides the field that needs attention.

      Section level — press Try again, it really retries
      QC results · plate P-118

      Could not load QC results

      The results service did not respond in time. Nothing was lost — the run is still queued and the rest of this page is unaffected.

      Reference QC-4417-A3F1

      Page level — the whole route failed
      Digital Design Studio · Workspace

      Something went wrong

      We hit an unexpected error loading this workspace. Your work is saved. If reloading does not help, send us the reference below and we will pick it up from the logs.

      Reference WS-2026-08-04-9C21

      Do

      Give the user a reference they can quote and say plainly what state their data is in.

      Don't

      Don't show a raw stack trace or 'Error: undefined'; log the real thing server-side.

      Overlays

      Anything that floats above the page: all of these manage focus, close on Escape, and draw their stacking order from the z-index scale.

      Drawer #

      A panel that slides in from the edge for work too long or too referential for a modal: a filter builder, a record's full detail. Left for context, right for detail and actions.

      Not for a single yes or no, and not for anything a user may want to link to — at that point it is a page.

      Sides
      Do

      Give a drawer the same four exits a modal has, and pin its primary action in a footer that does not scroll.

      Don't

      Do not let a drawer silently discard typed input on an overlay click.

      Study navigator

      STU-118 · Phase II · 14 sites

      Cohorts

      • C-201 — screening, 1,204 subjects
      • C-204 — responders at week 12, 318 subjects
      • C-207 — biomarker-high subset, 92 subjects
      • C-211 — safety follow-up, 1,180 subjects

      Assays

      • Targeted panel — 41 analytes, 6 runs
      • Flow cytometry — 12 panels, 4 runs

      Cohort filters

      Applied to the subject list behind this panel. Nothing is saved until you apply.

      Exclusions

      318 of 1,204 subjects

      Popover #

      A small panel anchored to the control that opened it, for secondary detail or compact controls. Non-modal — the page stays live and it light-dismisses on the next click outside.

      Not for anything the user has to acknowledge, and not for content long enough to need its own scrollbar.

      Placement and collision
      Interactive content
      Do

      Anchor it to its trigger, keep it under roughly two hundred words, and move focus in if it has controls.

      Don't

      Do not open a popover on hover, nest one inside another, or put a required step in it.

      Tooltip #

      A few words naming a control whose icon alone does not carry its purpose. It appears after a short hover delay, immediately on keyboard focus, and leaves the moment either ends.

      Never the only place a piece of information appears — a tooltip is unreachable on touch and absent from print.

      Placements
      Copy cohort ID Export as CSV Re-run QC Lock cohort
      Do

      Use two or three words matching the control's accessible label, on hover and on focus.

      Don't

      Do not put a link, a button, or a close control inside a tooltip; nobody can reach it.

      Confirmation dialog #

      The one dialog shape reserved for a decision that cannot be undone from the interface. Same width, same button order and the same wording every time, so it is recognised before it is read.

      Not for anything reversible — confirming everything trains people to click through without reading.

      Destructive confirm

      Nothing has been archived or deleted.

      Do

      Name the object and the consequence in the title, label the button with the verb, and focus Cancel.

      Don't

      Do not use a confirmation where an Undo toast would do, or make the destructive button dominant.

      Archive cohort C-204?

      The 318 subjects stay in the study and their samples are untouched. Saved analyses that reference this cohort will show it as archived and stop refreshing.

      Delete study STU-118?

      This deletes 4 cohorts, 15 assay runs and 1,204 subject records. There is no restore, and no export is taken first.

      The Delete button stays disabled until the identifier matches exactly.

      Data display

      Reading surfaces, weighted toward the dense, dashboard-style work the client projects behind this studio actually are.

      Table #

      For comparing many records across the same attributes — sample QC across a sequencing run, enrolment across sites. It earns its weight when the columns are scannable and the numbers line up.

      Not for one record's fields, and if fewer than three columns carry information it is a list with extra chrome.

      Basic
      HELIOS-2 sequencing samples — first four records.
      Sample Assay Site Run date Reads (M) QC
      HLS2-0114 RNA-seq Rotterdam 02 Jun 2026 41.8 Pass
      HLS2-0115 RNA-seq Rotterdam 02 Jun 2026 9.4 Fail
      HLS2-0121 WGS Basel 04 Jun 2026 118.6 Pass
      HLS2-0122 WGS Basel 04 Jun 2026 103.2 Pass
      Search, filter, sort, select, paginate — all live

      0 of 14 samples selected

      HELIOS-2 sequencing samples with QC verdict. Sortable by sample, assay, site, run date and read count.
      QC
      HLS2-0114 RNA-seq Rotterdam 02 Jun 2026 41.8 Pass
      HLS2-0115 RNA-seq Rotterdam 02 Jun 2026 9.4 Fail
      HLS2-0121 WGS Basel 04 Jun 2026 118.6 Pass
      HLS2-0122 WGS Basel 04 Jun 2026 103.2 Pass
      HLS2-0130 Methylation Uppsala 09 Jun 2026 27.5 Flagged
      HLS2-0131 Methylation Uppsala 09 Jun 2026 31.0 Pass
      HLS2-0142 scRNA-seq Kyoto 11 Jun 2026 12.7 Flagged
      HLS2-0143 scRNA-seq Kyoto 11 Jun 2026 8.1 Fail
      HLS2-0150 Exome Boston 16 Jun 2026 74.9 Pass
      HLS2-0151 Exome Boston 16 Jun 2026 68.2 Pass
      HLS2-0158 RNA-seq Basel 18 Jun 2026 52.6 Pass
      HLS2-0159 scRNA-seq Rotterdam 23 Jun 2026 19.3 Flagged
      HLS2-0164 WGS Uppsala 25 Jun 2026 96.4 Pass
      HLS2-0165 Exome Kyoto 30 Jun 2026 5.7 Fail
      Showing 1–8 of 14
      Density
      Comfortable — default
      Comfortable density example.
      Sample Assay Site Reads (M)
      HLS2-0150 Exome Boston 74.9
      HLS2-0151 Exome Boston 68.2
      HLS2-0158 RNA-seq Basel 52.6
      Compact — long working sessions
      Compact density example.
      Sample Assay Site Reads (M)
      HLS2-0150 Exome Boston 74.9
      HLS2-0151 Exome Boston 68.2
      HLS2-0158 RNA-seq Basel 52.6
      Do

      Right-align numeric columns, set tabular-nums, and sort them with a numeric comparator.

      Don't

      Don't sort a numeric column with localeCompare; it ranks 103.2 above 12.7 and still looks ordered.

      Card #

      One object's summary — a label of context, a title, a line or two of detail, at most one action — on a self-contained surface. Suits browsable collections of distinct things.

      Not for uniform records that need comparing field by field, and not as a layout device.

      Static vs. clickable vs. selected
      Static

      Cohort · HELIOS-2

      Batch RB-14

      126 libraries across four sites. Mean Q30 94.1%, two re-queued for low yield.

      Clickable · hover
      Selected
      Try it — hover, then Tab into it

      Static

      Nothing happens

      No cursor change, no lift, nothing focusable. A card that cannot be clicked must not behave as if it can.

      Do

      Give a clickable card exactly one link or button, stretched across the surface with an ::after overlay.

      Don't

      Don't nest a second button or link inside a clickable card.

      List #

      A sequence where each row is one thing with one or two attributes — collaborators, recent runs, attached protocol files. Cheaper to scan than cards, lighter to build than a table.

      Once rows need more than a leading element, two lines, and one trailing value, you are building a table.

      Simple and two-line
      Simple + trailing value
      • Rotterdam 38 samples
      • Basel 31 samples
      • Uppsala 29 samples
      • Kyoto 28 samples
      Leading avatar · two-line
      • Ines Moreau Ines Moreau Signed off batch RB-14 2h ago
      • Portrait of Devi Raman Devi Raman Re-queued HLS2-0115 for low yield 6h ago
      • HELIOS-2 assay protocol v4 PDF · 1.2 MB v4.0
      Interactive rows — click or Tab to select
      Row states
      Default
      Hover
      Focus
      Active
      Selected
      Do

      Keep one scannable value per row, in the same position on every row.

      Don't

      Don't build a list from divs with click handlers; rows that navigate are links, rows that act are buttons.

      Avatar #

      Identify a person at a glance — the owner column of a run table, a list of reviewers. Falls back to initials, and can carry a presence dot where presence changes what someone does.

      Not decoration: if who did it matters to the task, the name has to be in the text too.

      Photo, initials, presence
      Photo Portrait of Devi Raman
      Initials Ines Moreau
      Online Ines Moreau, online
      Away Tomas Oyelaran, away
      Offline Sara Kwon, offline
      Sizes
      xs · 24 Ines Moreau
      sm · 32 Ines Moreau
      md · 40 Ines Moreau
      lg · 56 Ines Moreau
      With a name, and stacked
      Paired with text Devi Raman Lab lead, Basel · Status: Online
      Stacked group
      • Devi Raman
      • Ines Moreau
      • Sara Kwon
      • and 3 more reviewers
      Do

      Pair every avatar with the person's name, and give presence dots a shape difference as well as a colour.

      Don't

      Don't let a status dot carry meaning through colour alone at 11px.

      Stat / KPI card #

      One number a person actually watches, its label, and how it has moved since the last comparable period. The first thing anyone looks at when a run goes wrong.

      Not for numbers nobody acts on — a row of eight tiles where two matter teaches people to skip the row.

      KPI row
      Samples sequenced · 7d 462 Up 10.5% vs. prior week
      Median Q30 94.1% No change, 0.0 pp vs. prior week
      QC failure rate 4.1% Down 0.5 pp vs. prior week
      Delta variants — coloured by meaning, not by sign
      Positive
      Sites enrolling 5/ 6 Up 1 site
      Negative
      Runs completed on time 82.4% Down 5.1 pp
      Neutral
      Assays in panel 5 No change, unchanged
      Do

      Colour the delta by whether the change is good, not by its sign.

      Don't

      Don't put eight tiles on a dashboard because you have eight numbers.

      Accordion #

      Collapse long, independently useful sections so the shape of the page stays visible — protocol caveats, methodology notes, release history. Good when a reader needs one block of several.

      Never behind an accordion: anything required to finish the current task, and form fields in particular.

      Single-open — panels are alternatives

      Libraries are prepared on the site's own automation line, so index sets differ from the central protocol. Reads below 15 M are re-queued rather than flagged.

      Multi-open — panels are reference material

      Any library under 10 M reads, or with Q30 below 90%, fails outright. Between 10 M and 15 M it is flagged for a reviewer rather than failed automatically.

      Once, under the original sample ID. The dashboard reports attempts separately so the failure rate is not inflated by successful re-runs.

      Do

      Pick single-open for alternatives and multi-open for reference material, and say which one in the props.

      Don't

      Don't put required form fields inside a panel, where a validation error can fire somewhere invisible.