Theming
Set the colour mode, contrast, density, accent and font, and add your own brand.
You theme Cadence with attributes on the <html> element and two CSS variables, or all at once with a brand. Status colours are not themeable.
<html data-mode="dark" data-contrast="more" data-density="comfortable" data-accent="indigo"></html>Attributes
| Attribute | Values | When it is not set |
|---|---|---|
data-mode | light, dark | Follows the operating system's prefers-color-scheme. |
data-contrast | standard, more | Follows the operating system's prefers-contrast. |
data-density | compact, comfortable | Compact, or the touch default on a touch device. |
data-accent | teal, blue, indigo, violet, plum, slate | teal |
data-brand | The name of a brand you have generated | No brand. A brand takes precedence over data-accent. |
Set the attributes on <html> only. Cadence does not support a differently themed subtree.
Mode
To let a reader choose, use the Mode toggle. It sets data-mode, removes it for the device's setting, and remembers the choice.
The light and dark classes on <html> state the mode as data-mode does. A theme switcher that works by class, such as next-themes with attribute="class", therefore needs no configuration. Either class overrules the operating system, so a reader on a dark operating system who chooses light gets light.
Contrast
Standard contrast meets WCAG 2.2 AA: 4.5:1 for text and 3:1 for control boundaries. more raises those to 7:1 and 4.5:1.
Every pairing of tokens that a component relies on is asserted in the @cadence-clinical/tokens test suite, for each mode, contrast level and accent. A token change that lowers a pairing below its minimum fails the build.
Density
Compact is the default. Controls are 32px high, which suits a workstation with a mouse.
A touch screen that has not set data-density, such as a phone or a tablet, gets the touch default: 40px controls with 14px text, so a small screen holds more while a finger still has room. Set comfortable for 44px targets, for gloved hands, workstations on wheels, or anyone who asks for larger controls. A large control is 44px at the touch default too.
To compare them on this site, choose Density beside the light and dark switch. Automatic leaves data-density off, so the density follows your device. Storybook has the same choice in its toolbar.
Content follows the same setting, so a card and the controls inside it change size together:
| Utility | Compact | Touch default | Comfortable | Use |
|---|---|---|---|---|
h-control-sm, h-control, h-control-lg | 28, 32, 36px | 36, 40, 44px | 36, 44, 48px | The height of a control |
px-control-x, gap-control-gap | 10, 6px | 12, 8px | 14, 8px | A control's padding and gap |
text-control-sm, text-control, text-control-lg | 12, 13, 14px | 13, 14, 15px | 14, 15, 16px | The text of a control at each size, and its label |
size-control-icon | 16px | 18px | 20px | An icon inside a control |
size-control-indicator | 16px | 18px | 20px | A checkbox, a radio and a switch's thumb |
text-body | 13px | 14px | 15px | The text of a card, an item or a field |
text-title | 15px | 16px | 17px | The title of a card or an item |
p-container, p-container-sm | 12, 8px | 12, 10px | 16, 12px | The padding of a card or an item |
A field's text is 16px on any touch screen, whatever the density: a smaller size makes Safari on iOS zoom the page when the field takes focus.
Use these in your own components in place of fixed sizes such as text-sm and p-4.
Font
The default typeface is Public Sans, a variable font in upright and italic. @cadence-clinical/ui/styles.css loads it, and so does the registry's theme, so there is nothing to set up. Its figures line up in columns wherever a component asks for tabular numbers.
If you import @cadence-clinical/tokens/theme.css yourself, add the font beside it. Without it, Cadence falls back to the system typeface.
@import "@cadence-clinical/tokens/theme.css";
@import "@cadence-clinical/tokens/fonts.css";To use your own typeface, set the variables. A browser downloads a font only when text uses it, so Public Sans then costs nothing.
:root {
--cadence-font-sans: "Inter", sans-serif;
--cadence-font-mono: "JetBrains Mono", monospace;
}The monospaced font stays the system's. Use font-mono for an identifier or a code: in Public Sans a capital I and a lowercase l look alike.
Status colours
critical, warning, success and info carry clinical meaning, so they are the same in every theme and no attribute changes them. Each status has five tokens:
| Token | Use |
|---|---|
critical | A solid fill. |
critical-foreground | Text and icons on the solid fill. |
critical-subtle | A tinted surface. |
critical-text | Text on the page background or on the tinted surface. |
critical-border | The edge of any status surface. |
Put the border token on every status surface. No amber fill reaches 3:1 against a white page, so without its border a warning surface has no perceivable edge.
Never signal status with colour alone. Pair it with text or an icon.
Severity scale
The severity scale colours the bands of an observation chart and the cells of an observation table. It has seven fixed, ordered steps, severity-0 to severity-6. Like the status colours, they are the same in every theme and brand.
| Step | Colour |
|---|---|
severity-0 | No tint: within range. |
severity-1 | Yellow. |
severity-2 | Orange. |
severity-3 | Light red. |
severity-4 | Red. |
severity-5 | Deep red. |
severity-6 | Purple, for an emergency. |
Each step has three tokens:
| Token | Use |
|---|---|
severity-2 | A chart marker. It reaches 3:1 against the page. |
severity-2-subtle | The fill of a band or a cell. Body text sits on it. |
severity-2-border | The edge of a band or a cell. |
You do not choose a step's colour. An observation schema gives each of its levels a step, such as "Score 2" at severity-2, and the step's colour is fixed. Adjacent steps are close in colour, as they are on paper charts, so a level is always shown with its words and a marker's shape as well as its colour.
Add your own accent
An accent is brand colour only. It drives primary buttons, links and focus rings.
createAccent takes one brand colour and returns a complete accent. It keeps the hue and chroma, and solves the lightness of each role so that contrast holds in both modes and both contrast levels.
import { writeFile } from "node:fs/promises";
import { assertAccent, generateAccentCss } from "@cadence-clinical/tokens";
const definition = assertAccent("#0b5fff");
await writeFile("app/brand-accent.css", generateAccentCss("brand", definition));Import the generated file after the Cadence styles, and set data-accent="brand" on <html>.
assertAccent throws when a colour cannot be made safe. Run it in your build so that an unsafe accent fails the build.
Colours Cadence rejects
Cadence rejects an accent whose hue is within 30° of critical red, warning amber or success green in OKLCH. On a clinical screen, a brand colour in those ranges can be read as a status. If your brand is red, amber or green, use slate or another curated accent, and keep the brand colour in your logo.
Greys are exempt from the hue check because a grey cannot be mistaken for a status.
Add your own brand
A brand is a preset for the whole page: an accent, the surfaces and text of light mode, and fonts. Set it with data-brand on <html>.
To see one, choose Theme beside the light and dark switch on this site, or in the Storybook toolbar. Live examples sit on a card, so a brand's page colour shows around them.
- Midnight sets a grey page, navy text and buttons, a blue focus ring, and Geist.
- Lagoon keeps a white page. It sets teal text, blue buttons, a purple focus ring, pale purple and green surfaces, and Figtree.
- Cobalt keeps a white page. It sets navy text, bright blue buttons and links, light grey surfaces, and Work Sans.
- Mulberry keeps a white page. It sets dark grey text, plum buttons and links, light grey surfaces, and Source Sans 3.
What a brand can set
import { writeFile } from "node:fs/promises";
import { generateBrandCss, type Brand } from "@cadence-clinical/tokens";
const brand: Brand = {
label: "Our health service",
accent: "#25215d",
light: {
background: "#f4f3f5",
foreground: "#25215d",
card: "#ffffff",
input: "#85879b",
ring: "#005ca9",
},
font: { sans: '"Geist Variable", system-ui, sans-serif' },
};
await writeFile("app/brand.css", generateBrandCss("ours", brand));Import the generated file after the Cadence styles, and set data-brand="ours" on <html>. A brand names its fonts. Load them yourself.
accentis one brand colour, which Cadence solves as it does for your own accent.lightsets surfaces, their text, borders and the accent's colours, for light mode at standard contrast. A token you leave out keeps Cadence's value.fontsets--cadence-font-sansand--cadence-font-mono.
background is the page, and fields, checkboxes and outline buttons fill with it. Inside a card, a dialog or a popover they fill with that surface's colour instead, so a brand with a grey page still has white fields on its cards.
What a brand cannot change
- Status colours. Critical, warning, success and info look the same in every brand, so a clinician reads them the same way wherever they work.
- Dark mode and more contrast. In those modes the page keeps Cadence's surfaces, with the brand's accent solved for them. A brand cannot make an accessibility setting harder to read.
generateBrandCss checks every pairing a component relies on, in both modes and at both contrast levels, and throws when one falls short. A common failure is the input token: it draws the edge of a form field, which needs 3:1 against the page and a card. A decorative border colour is usually too pale for it.