Colour
How we use colour so it carries meaning consistently, meets accessibility, and works in light and dark across every Single Record product.
How colour is organised
Colour is held in three tiers, and which tier you are allowed to touch depends on what you are doing.
- Primitives are the raw palette, for example
--color-blue-800. They carry no meaning. Never reference a primitive from a component. - Semantic tokens carry the meaning, for example
--sr-color-interactive-primary. This is the tier you build with. - Component tokens exist only where one part of a component needs a value that cannot be expressed semantically.
Picking by role rather than by hue is what makes dark mode and future palette changes possible. A component that asks for "the primary interactive colour" keeps working when that colour changes. A component that asks for "Blue 800" does not.
/* Correct: ask for the role. */
.sr-panel {
background: var(--sr-color-surface-section-cards);
border: 1px solid var(--sr-color-border-default);
color: var(--sr-color-text-primary);
}
/* Wrong: a primitive, and a raw hex. */
.sr-panel {
background: var(--color-blue-50);
border: 1px solid #d8dde0;
}
Semantic tokens
These are the tokens to build with. Every value below is read from the published token artifact, so this table cannot drift from what ships.
Interactive
| Token | Value | Use for |
|---|---|---|
--sr-color-interactive-primary |
#325083 | Primary actions and the main brand fill |
--sr-color-interactive-primary-hover |
#1e3050 | Hover step for the primary fill |
--sr-color-interactive-secondary |
#1b294a | Secondary emphasis and dark chrome such as the masthead |
--sr-color-interactive-link |
#005aa8 | Text links in body content |
--sr-color-interactive-destructive |
#d5281b | Irreversible actions, paired with a confirmation step |
--sr-color-interactive-disabled |
#828dac | Disabled control fills |
Surface
| Token | Value | Use for |
|---|---|---|
--sr-color-surface-background |
#f4f5f8 | The page itself |
--sr-color-surface-section-cards |
#ffffff | Large panels and page sections |
--sr-color-surface-small-cards |
#ffffff | Cards, menus and popovers |
--sr-color-surface-accent |
#ebf5fa | Selected rows and highlighted regions |
--sr-color-surface-subtle |
#f4f5f8 | Hover surfaces and quiet fills |
Text
| Token | Value | Use for |
|---|---|---|
--sr-color-text-primary |
#212b32 | Body copy, headings and data values |
--sr-color-text-secondary |
#4c6272 | Supporting text, hints and metadata |
--sr-color-text-inverse |
#ffffff | Text on a saturated or dark fill |
--sr-color-text-disabled |
#707488 | Text in a disabled control |
Border
| Token | Value | Use for |
|---|---|---|
--sr-color-border-subtle |
#f0f4f5 | Row dividers and quiet separators |
--sr-color-border-default |
#d8dde0 | Standard component and panel borders |
--sr-color-border-strong |
#768692 | Emphasis borders and table header rules |
--sr-color-border-focus |
#0d8bad | The focus ring, in both light and dark mode |
--sr-color-border-disabled |
#9ea1af | Borders on disabled controls |
Contrast
Body text and essential UI must meet WCAG 2.2 AA: 4.5:1 for text, and 3:1 for large text, icons and borders. We target AAA where the pairing allows it. Validate a token against the surface it will actually sit on, not against white by default.
| Role | Foreground | On white | Paired surface | On that surface |
|---|---|---|---|---|
| Critical | --sr-color-status-critical |
6.70:1 | --sr-color-status-critical-surface |
6.16:1 |
| Success | --sr-color-status-success |
7.14:1 | --sr-color-status-success-surface |
6.69:1 |
| Warning | --sr-color-status-warning |
1.55:1 | --sr-color-status-warning-surface |
1.49:1 |
| Information | --sr-color-status-info |
6.94:1 | --sr-color-status-info-surface |
6.31:1 |
Ratios are computed at build time from the published token values.
Accents are not text colours
The brand accent --sr-color-brand-accent is an accent only. It does not carry white
text at body size, so never use it to fill a button or any surface that needs white text on it. Use
--sr-color-interactive-primary for that.
Never colour alone
Colour is never the only signal. This is a hard requirement, not a preference: it is WCAG 1.4.1, and in a clinical context a status that only reads as "red" is a patient-safety problem.
- Pair a status colour with text, and usually an icon as well.
- A selected table row carries a non-colour signal too, such as a selection control or a left accent border.
- Form validation states pair the colour with an error message tied to the field.
- Review screens in greyscale. If you cannot tell the states apart, it fails.
Focus
The focus ring is --sr-color-border-focus at #0d8bad, and it is the same colour
in light and dark mode so that focus behaves predictably wherever a control appears. It scores
3.63:1 against the page background and 2.04:1 against the primary fill, so it
stays visible on both.
There is no separate focus primitive. An earlier yellow focus colour was replaced by this one and has been removed from the palette, so if you find a yellow focus ring anywhere it is stale code.
.sr-button:focus-visible {
outline: 3px solid var(--sr-color-border-focus);
outline-offset: 2px;
}
Dark mode
Dark mode is a second set of values behind the same semantic names. A component written against semantic tokens needs no dark-mode code of its own. Because the names do not change, the rule stays the same: never reach past the semantic layer to a primitive, or the component will not follow the theme.
The dark theme is built and published, but the theme switch on this site is turned off while the colour work is being finalised.
The palette
The raw primitives, rendered live from the published tokens. Reference these from semantic tokens only, never from a component. The number on each swatch is its step.
Blue, NHS Wales blue and the primary brand
--color-blue-900#1e3050--color-blue-850#2c3e72--color-blue-800#325083--color-blue-700#3d6199--color-blue-600#4c72ae--color-blue-500#5c6991--color-blue-400#828dac--color-blue-300#aab1c6--color-blue-200#d4d8e2--color-blue-100#eceef3--color-blue-50#f4f5f8Cyan, DHCW blue, used for the accent and the focus ring
--color-cyan-900#0a6a84--color-cyan-850#0c7b99--color-cyan-800#0d8bad--color-cyan-700#12a3c9--color-cyan-600#71accd--color-cyan-500#8dc0da--color-cyan-400#afd4e5--color-cyan-300#d6eaf2--color-cyan-100#ebf5fa--color-cyan-50#f4fafcNavy
Navy is deliberately a short ramp. It is a narrow utility family used for dark surfaces, disabled borders and disabled text, and steps are added only when a semantic token needs one.
--color-navy-900#1b294a--color-navy-700#464c64--color-navy-500#707488--color-navy-300#9ea1af--color-navy-100#cdcfd6Grey, the neutral UI ramp
--color-grey-900#212b32--color-grey-800#2c3a44--color-grey-700#3b4e5b--color-grey-600#4c6272--color-grey-500#768692--color-grey-400#aeb7bd--color-grey-300#c6cdd1--color-grey-200#d8dde0--color-grey-100#f0f4f5--color-grey-50#f7fafaRed, critical and destructive
--color-red-900#5f110a--color-red-800#8b190f--color-red-700#b32014--color-red-600#d5281b--color-red-500#e03a31--color-red-400#ec5e56--color-red-300#f48b85--color-red-200#f9b5b1--color-red-100#fcdbd9--color-red-50#fef3f2Green, success
--color-green-900#003319--color-green-800#004d24--color-green-700#006630--color-green-600#007f3b--color-green-500#1fa66d--color-green-400#4ab88b--color-green-300#7bcaaa--color-green-200#aeddc9--color-green-100#d9efe5--color-green-50#f0faf4Yellow, warning
--color-yellow-900#4a3000--color-yellow-800#6b4400--color-yellow-700#8a5a00--color-yellow-600#b47800--color-yellow-500#f8ca4d--color-yellow-400#face62--color-yellow-300#fbd97f--color-yellow-200#fde8ab--color-yellow-100#fdf3d7--color-yellow-50#fffaebInfo blue
--color-info-blue-900#002e5c--color-info-blue-800#004483--color-info-blue-700#005aa8--color-info-blue-600#0d62a3--color-info-blue-500#267ab8--color-info-blue-400#4e95ca--color-info-blue-300#7eb0d9--color-info-blue-200#aecce8--color-info-blue-100#d6e8f5--color-info-blue-50#eef5fcAccessibility requirements
| Requirement | WCAG SC | How Single Record meets it | Test method |
|---|---|---|---|
| Text meets contrast against its surface | 1.4.3 | Every text and surface pairing is checked before a token ships; the ratios are published on this page. | Automated contrast |
| Non-text UI meets contrast | 1.4.11 | Borders, icons and control boundaries are checked at 3:1 against adjacent colours. | Automated contrast |
| Colour is not the only signal | 1.4.1 | Status colours pair with text and an icon; selected rows carry a non-colour indicator. | Greyscale review |
| Focus is visible on every surface | 2.4.7 / 1.4.11 | One focus colour in both modes, checked against page, card and saturated fills. | Keyboard tab, contrast check |
| Meaning survives a theme change | 1.4.1 | Light and dark are two value sets behind one set of semantic names, so meaning is carried by the name. | Theme switch review |
| Users can override colours | 1.4.12 / 1.4.8 | Colours are CSS custom properties, so forced-colours and user stylesheets apply cleanly. | Forced colours mode |