PREDICT uses a small set of colour tokens. Components never use raw colours; they use tokens like bg-primary or text-muted-foreground, which switch automatically between light and dark mode.

How colour works

  • Tokens are CSS variables. Each token, such as --primary, is defined twice in packages/ui/src/styles/globals.css: once in :root for light mode and once in .dark for dark mode.
  • Values are OKLCH. OKLCH keeps lightness even across hues, so the purple scale and the status colours step evenly. Hex values on this page are worked out from the OKLCH source for reference.
  • Tailwind classes map to tokens. @theme inline turns --primary into bg-primary, text-primary, border-primary, fill-primary, and so on. Add an opacity with a slash, such as bg-primary/10.

Brand palette

Mostly black and white, with Predict Purple (#8504FE) as the one brand colour and lilac as its soft partner. Use purple for the main action and brand moments, not large areas. See Brand Guidelines for proportions and logo colours.

Tokens

Every token, in light and dark mode. Click a token name to copy it.

Surfaces

TokenLightDark

The page.

#FFFFFFoklch(1 0 0)
#000000oklch(0 0 0)

Cards and panels.

#FFFFFFoklch(1 0 0)
#000000oklch(0 0 0)

Popovers, menus, and tooltips.

#FFFFFFoklch(1 0 0)
#000000oklch(0 0 0)

Text

TokenLightDark

Headings and main text.

#000000oklch(0 0 0)
#FFFFFFoklch(1 0 0)

Descriptions, captions, and hints.

#161616oklch(0.2 0 0)
#BEBEBEoklch(0.8 0 0)

Text on cards.

#000000oklch(0 0 0)
#FFFFFFoklch(1 0 0)

Text in popovers.

#000000oklch(0 0 0)
#FFFFFFoklch(1 0 0)

Brand

TokenLightDark

Predict Purple. The main action and brand moments.

#8504FEoklch(0.536 0.2917 295.68)
#8504FEoklch(0.536 0.2917 295.68)

Text and icons on primary.

#FFFFFFoklch(1 0 0)
#FFFFFFoklch(1 0 0)

Lilac. Secondary buttons and soft highlights.

#F4EEFFoklch(0.961 0.0343 295.7)
#28114Doklch(0.253 0.1037 295.7)

Text on secondary.

#030303oklch(0.1 0 0)
#DEDEDEoklch(0.9 0 0)

Neutrals

TokenLightDark

Quiet fills, such as skeletons and tracks.

#BEBEBEoklch(0.8 0 0)
#161616oklch(0.2 0 0)

Hover and selected fills.

#9E9E9Eoklch(0.7 0 0)
#2E2E2Eoklch(0.3 0 0)

Text on accent.

#2E2E2Eoklch(0.3 0 0)
#9E9E9Eoklch(0.7 0 0)

Lines and focus

TokenLightDark

Dividers and card edges. The default border colour.

#DEDEDEoklch(0.9 0 0)
#2E2E2Eoklch(0.3 0 0)

Input borders and fills.

#DEDEDEoklch(0.9 0 0)
#2E2E2Eoklch(0.3 0 0)

Focus rings.

#9E9E9Eoklch(0.7 0 0)
#484848oklch(0.4 0 0)

Feedback

TokenLightDark

Delete actions and errors.

#DF2225oklch(0.58 0.22 27)
#DF2225oklch(0.58 0.22 27)

Sidebar

TokenLightDark

Sidebar background.

#DEDEDEoklch(0.9 0 0)
#030303oklch(0.1 0 0)

Sidebar text.

#030303oklch(0.1 0 0)
#DEDEDEoklch(0.9 0 0)

Active item in the sidebar.

#030303oklch(0.1 0 0)
#DEDEDEoklch(0.9 0 0)

Text on the active item.

#DEDEDEoklch(0.9 0 0)
#030303oklch(0.1 0 0)

Sidebar hover fill.

#BEBEBEoklch(0.8 0 0)
#161616oklch(0.2 0 0)

Text on sidebar hover.

#161616oklch(0.2 0 0)
#BEBEBEoklch(0.8 0 0)

Sidebar dividers.

#DEDEDEoklch(0.9 0 0)
#2E2E2Eoklch(0.3 0 0)

Focus rings in the sidebar.

#9E9E9Eoklch(0.7 0 0)
#484848oklch(0.4 0 0)

Pairs

Most surface tokens come with a -foreground partner for the text on top of it. Always use them together:

SurfaceText on it
bg-backgroundtext-foreground, text-muted-foreground
bg-cardtext-card-foreground
bg-popovertext-popover-foreground
bg-primarytext-primary-foreground
bg-secondarytext-secondary-foreground
bg-accenttext-accent-foreground
bg-sidebartext-sidebar-foreground

destructive has no foreground token. Use white text on it, or use it as text and borders on a light tint, such as bg-destructive/10 text-destructive.

Status colours

Five colours show how far a biomarker result is from healthy. They follow the Z-score (how many SDs from the healthy mean), and they are the same in light and dark mode.

NormalInside the healthy range.--status-normal · #00C950 · oklch(0.723 0.219 149.58)
Normal
MildOut of range, |Z| under 3.--status-mild · #F0B100 · oklch(0.795 0.184 86.05)
Mild
Moderate|Z| from 3 to under 4.--status-moderate · #2B7FFF · oklch(0.623 0.214 259.81)
Moderate
Severe|Z| from 4 to under 5.--status-severe · #FF6900 · oklch(0.705 0.213 47.6)
Severe
Extreme|Z| of 5 or more, or past a critical limit.--status-extreme · #FB2C36 · oklch(0.637 0.237 25.33)
Extreme

These are the thresholds used by Reference Range and the Bio Clock severity scale. A low value on the harmless side of a directional range, such as a low hs-CRP, is not a concern and shows in neutral grey.

  • Always pair colour with a word or icon. About one in twelve men has some colour blindness, and green and orange are easy to confuse. Show "High" or "Normal" next to the colour.
  • Use the tint pattern for text. On white, the status colours are too light for body text (see the contrast table). Use a tinted badge, bg-status-mild/15 text-status-mild, or a solid fill with a label beside it.
  • Keep them for results. Don't use status colours for decoration, brand, or general success and error messages. Use destructive for errors.

Chart colours

A sequential purple scale for charts, from chart-1 (lowest) to chart-5 (highest). In dark mode the scale gets darker so the light end doesn't glare.

--chart-1#FDFCFF
#000000
--chart-2#F4EEFF
#28114D
--chart-3#E3D3FF
#431A7D
--chart-4#C8AAFF
#6033A9
--chart-5#8504FE
#8504FE
The scale in the current theme. Each swatch shows light on the left and dark on the right.
  • Use chart-5 for the main or only series, and lighter steps for comparisons and backgrounds.
  • The scale shows amount, not category. For results by status, use the status colours.
  • chart-1 is almost the page colour, so give it a border or use it only for backgrounds.

Contrast

WCAG asks for a contrast of 4.5:1 for body text and 3:1 for large text (24px, or 19px bold) and icons. Ratios below are worked out from the token values.

PairLightDark
foreground on background
Aa21.00:1AAA
Aa21.00:1AAA
muted-foreground on background
Aa18.10:1AAA
Aa11.24:1AAA
primary-foreground on primary
Aa6.09:1AA
Aa6.09:1AA
primary on background
Aa6.09:1AA
Aa3.45:1AA large
secondary-foreground on secondary
Aa18.14:1AAA
Aa12.17:1AAA
foreground on muted
Aa11.24:1AAA
Aa18.10:1AAA
accent-foreground on accent
Aa5.10:1AA
Aa5.10:1AA
destructive on background
Aa4.78:1AA
Aa4.39:1AA large
white on destructive
Aa4.78:1AA
Aa4.78:1AA
status-normal on background
Aa2.22:1Fail
Aa9.45:1AAA
status-mild on background
Aa1.91:1Fail
Aa10.97:1AAA
status-moderate on background
Aa3.76:1AA large
Aa5.58:1AA
status-severe on background
Aa2.89:1Fail
Aa7.26:1AAA
status-extreme on background
Aa3.82:1AA large
Aa5.50:1AA

What this means in practice:

  • foreground, muted-foreground, secondary-foreground, and accent-foreground pass for any text size, in both modes.
  • White on primary passes for body text (6.1:1). primary as text on white also passes; on black it only passes for large text (3.5:1).
  • destructive as text passes on white (4.8:1) but only for large text on black (4.4:1). In dark mode, pair it with an icon or use it as a fill.
  • On white, no status colour passes for body text, and normal, mild, and severe are below 3:1 even for icons. Use them as fills and tints with a label, as above. On black, all five pass.

Using colours in code

Use token classes in components. Don't hard-code hex values or Tailwind palette colours like text-green-500, which won't follow the theme.

Adding a token

  1. Add the variable to :root and .dark in globals.css, in OKLCH.
  2. Map it in @theme inline, such as --color-warning: var(--warning);.
  3. Use it as bg-warning, text-warning, and so on.
  4. Check its contrast against the surfaces it will sit on, in both modes.