Centrim Design System — a token-first, accessible React component system.
Colour
Colour carries brand and meaning — never decoration. Primitive ramps hold the raw values; components only ever read the semantic layer, so the whole system can be rebranded from one file.
What is this?
A two-layer colour system. Primitives (--ds-blue-500, --ds-neutral-200) are locked raw values. Semantic tokens (--color-primary, --color-text-secondary, --color-error) map those primitives onto roles, once per theme.
When to use it
- Any time a component needs a colour — always through a semantic token
- Communicating state: success, warning, error and info
- Establishing hierarchy between a primary action and everything around it
How to use it
- Read --color-* (or its Tailwind utility, e.g. bg-surface, text-text-secondary)
- Pair every background token with its matching -foreground token
- Use -subtle backgrounds for status surfaces and solid ramps for status marks
What to avoid
- Hex values, rgb(), or Tailwind palette classes such as bg-blue-600
- Reading a primitive ramp directly from a component
- Colour as the only signal for state — always pair it with an icon or text
Brand ramps
Centrim Blue 500 = #006DEA (locked). Yellow 500 = #F9C846 (locked) — a supporting accent, never the primary action.
--ds-blue-*
50
100
200
300
400
500
600
700
800
900
950
--ds-yellow-*
50
100
200
300
400
500
600
700
800
900
Neutrals
A slate-based ramp from neutral-0 (#FFFFFF) to neutral-950 (#020617). Text, surfaces and borders all resolve from here.
--ds-neutral-*
0
50
100
200
300
400
500
600
700
800
900
950
Status ramps
Each -500 carries white foreground text at 4.5:1 or better. Info stays visually distinct from Centrim Blue so status never reads as brand.
Semantic system tokens
The only colour layer a component may read. Toggle the theme in the sidebar to verify both mappings.
Primary
The single brand action colour. One primary action per view.
Secondary
Neutral-weighted supporting actions that must not compete with primary.
Accent
Highlight and emphasis only — never a call to action.
Background
The page canvas and its quiet variations.
Surface
Anything that sits on the canvas: cards, menus, floating panels.
Text
Three readable weights plus disabled, inverse and link.
Border
Structure and separation. Focus is its own token, never reused.
Status
Meaning only. Each status has a solid, a subtle and a foreground.
Role reference
| Token family | Role | Never |
|---|---|---|
| --color-primary-* | The one brand action per view | Large background fills or status meaning |
| --color-secondary-* | Supporting actions beside a primary | The main call to action |
| --color-accent-* | Emphasis, highlights, chart accents | Interactive controls or buttons |
| --color-background-* | The page canvas and quiet bands | Cards or floating panels |
| --color-surface-* | Cards, menus, floating panels | The page canvas |
| --color-text-* | Three readable weights plus disabled/inverse/link | Decorative tinting of body copy |
| --color-border-* | Structure, separation and focus | Filling a shape |
| --color-success|warning|error|info | State and meaning only | Brand expression |
Contrast rules
WCAG 2.2 AA is the floor, not the target
Body and label text meets 4.5:1 against its own background. Large display text and non-text UI boundaries (borders, focus rings, control outlines) meet 3:1.
--color-text-disabled is deliberately strengthened past the usual disabled grey so a disabled label stays legible while emphasis drops.
Do & don't
Do
- Use bg-surface + text-text-primary for content that sits on the canvas.
- Use the -subtle status background with its -subtle foreground for inline status.
- Reserve the primary blue for the single most important action on the screen.
- Verify every new pairing in both light and dark before shipping.
Don't
- Do not introduce a new colour without adding it to the token layer first.
- Do not tint text with brand or accent colour to make it feel designed.
- Do not use the accent yellow for buttons — its contrast profile is for emphasis.
- Do not invert light values to build dark mode; dark has its own designed mapping.
Why it is designed this way
Splitting primitives from semantics means a rebrand touches one mapping block instead of every component. It also makes dark mode a design decision rather than a filter: the dark theme re-points the same semantic names at different primitives, so surfaces stay layered and status colours stay distinguishable instead of washing out.