Design systems
UI palette: how to measure it instead of picking it by eye
A palette is not a set of colours: it is a map of roles with contrast thresholds. Here is how to test it in light and dark before an audit does.
Key points
- Define roles, not colours: canvas, surface, text, secondary text, border, accent.
- Normal text needs 4.5:1; large text and control borders need 3:1.
- The test that fails most often is secondary text on a surface, not the heading.
You judge a palette with two questions: what role does this colour play and what contrast does it produce on each surface. If the answer is “I like it”, you have not tested anything.
Six roles, not twenty colours
| Role | Use | Threshold |
|---|---|---|
| Canvas | Page background | - |
| Surface | Cards, panels, dialogs | It must differ from the canvas |
| Text | Body copy and headings | 4.5:1 (3:1 if large) |
| Secondary text | Metadata, helper text | 4.5:1 |
| Border | Dividers and inputs | 3:1 on controls |
| Accent | Primary action and focus | 3:1 against the canvas |
Six roles let you change the brand without rewriting components. Twenty named colours turn every theme into an exception.
Contrast is measured, not estimated
- Use a contrast checker with the exact token values, not the colours in the mockup.
- Measure every text/surface pair: a palette can pass on white and fail on grey.
- Check the `hover`, `disabled` and `focus` states too.
- Repeat everything in the dark theme: values are not inherited.
:root {
--ui-bg: 0 0% 100%;
--ui-surface: 240 20% 98%;
--ui-fg: 240 10% 4%;
--ui-fg-muted: 240 4% 38%;
--ui-border: 240 6% 88%;
--ui-accent: 221 83% 45%;
}
html.dark {
--ui-bg: 240 10% 4%;
--ui-surface: 240 6% 9%;
--ui-fg: 0 0% 98%;
--ui-fg-muted: 240 5% 70%;
--ui-border: 240 4% 18%;
--ui-accent: 217 90% 62%;
}
The three recurring mistakes
Grey on grey
Text #888 on a #f5f5f5 surface looks elegant and fails. Secondary text is the first thing to vanish, and it carries the useful information.
Accent as text
A bright accent works as a button and fails as text on a light canvas. Keep the action role and the information role apart.
Invisible focus
The focus ring is an interface element: it needs 3:1 against the adjacent colour. If you hide it because it “looks messy”, you remove orientation for keyboard users.
Check it in three steps
- Open the page and measure text, secondary text, borders and focus.
- Repeat with the dark theme and with the system theme.
- Check a dialog and a toast: can you tell the surfaces apart?
EneUI documents the token layer it uses in /components. The full component checklist sits in the article on accessible components.
Frequently asked questions
What is the minimum contrast ratio?
4.5:1 for normal text, and 3:1 for large text and for interface elements such as borders and focus rings.
Is contrast measured on the design colours?
On the final rendered values. A mockup can use a colour that the browser approximates differently.
Can a dark theme reuse the same colours?
No. The values need remapping: the same accent can glare or lose contrast on black.
Is checking the heading enough?
No. Secondary text and control borders are the places that fail most often.
How do I test the palette in a repeatable way?
Extract the tokens, measure the pairs with a script or a contrast checker, and save the output. The test then stays the same after every change.