Turn a Colour Palette into CSS Variables
A palette becomes useful in code when each colour has a job. Rather than scatter hex values through components, define a small set of reference colours and map them to semantic CSS custom properties such as --colour-text and --colour-action. Your components then describe their purpose, not the particular blue, green or neutral chosen today.
This guide uses the dark Midnight SaaS palette as a worked example. The same approach suits the quieter Nordic Frost palette, the earthy Warm Terracotta palette and the Forest Cabin palette. Start with the supplied colours, then test the pairs you intend to put together in an interface.
Start with two layers of variables
Keep raw palette values separate from the roles used by an interface. Reference variables are your approved swatches. Semantic variables point to the reference variable that fulfils a job: page background, default text, muted text, action, border or focus outline. This mirrors a token system in which a value can change without changing every component that uses the role.
:root {
/* Reference palette: only colour values live here */
--ref-midnight: #0F172A;
--ref-slate: #1E293B;
--ref-blue: #1D4ED8;
--ref-emerald: #10B981;
--ref-snow: #F8FAFC;
--ref-muted: #64748B;
/* Semantic tokens: use these in components */
--colour-canvas: var(--ref-snow);
--colour-surface: #FFFFFF;
--colour-text: var(--ref-midnight);
--colour-text-muted: var(--ref-muted);
--colour-border: #CBD5E1;
--colour-action: var(--ref-blue);
--colour-action-text: #FFFFFF;
--colour-focus: var(--ref-emerald);
}Declare global tokens on :root when the entire document needs them. Custom properties with the -- syntax inherit, so a value defined on a container can also be deliberately scoped to a widget or a themed section. Names are case-sensitive, so choose lower-case, hyphenated names and use them consistently.
Assign roles before styling components
Write down the visual jobs your product actually needs. A small site might only need canvas, surface, text, muted text, action, action text, border and focus. A more complex product can add success, warning, danger and information roles, but do not create a role merely because the source palette has another swatch.
Apply semantic variables in component rules. This avoids a misleading name such as --blue-button, which becomes wrong as soon as the button changes colour. It also prevents a component from silently introducing a one-off value.
.card {
background: var(--colour-surface);
border: 1px solid var(--colour-border);
color: var(--colour-text);
}
.button-primary {
background: var(--colour-action);
color: var(--colour-action-text);
}
.button-primary:focus-visible {
outline: 3px solid var(--colour-focus);
outline-offset: 3px;
}Use a fallback only where it improves resilience, for example in a reusable component that can be embedded outside its normal theme: color: var(--colour-text, #0F172A). A fallback is used when the referenced custom property is invalid or undefined. It is not a substitute for defining a complete theme.
Check contrast for each intended pair
Contrast belongs to a foreground-and-background pairing, not to a colour in isolation. For normal text, WCAG 2.2 AA requires at least 4.5:1. Large text has a 3:1 minimum, where large means at least 18 point regular or 14 point bold. Treat these as minimums, not permission to skip testing hover, disabled and dark-theme states.
In this example, #0F172A text on #F8FAFC has a 17.06:1 ratio. White text on #1D4ED8 has 6.70:1, and #64748B on white has 4.76:1. Each meets the normal-text threshold. The calculations use WCAG relative luminance, not a subjective impression of brightness. Check every actual combination, including text on an image or translucent layer. If the background is not fixed, do not claim a single contrast result.
Interactive controls and meaningful visual indicators need their own review. A focus indicator should be visible against its surrounding colours. Do not make colour the only signal for an error, selected tab or status. Pair it with a label, icon, border, pattern or position as appropriate.
Add a dark theme by remapping roles
Do not duplicate every component rule for dark mode. Keep the component rules above and change the semantic assignments in one context. The reference palette can be shared; the meaning of a reference colour changes with the theme.
[data-theme="dark"] {
--colour-canvas: #0F172A;
--colour-surface: #1E293B;
--colour-text: #F8FAFC;
--colour-text-muted: #CBD5E1;
--colour-border: #475569;
--colour-action: #3B82F6;
--colour-action-text: #0F172A;
--colour-focus: #FBBF24;
}Set data-theme="dark" on the document root or on a self-contained preview. For this worked pair, #FBBF24 against #0F172A is 10.69:1, which makes a strong focus outline. If your product follows the system preference, place the alternative semantic assignments in @media (prefers-color-scheme: dark); an explicit user choice can then override it.
Avoid common palette-to-CSS mistakes
- Using raw hex values in components: it makes a palette refresh slow and risky. Reserve hex, RGB or OKLCH values for the reference layer.
- Confusing a brand swatch with an accessible action colour: a primary swatch may work for a logo but fail as small button text. Choose or adjust a dedicated action token after testing.
- Changing only text or only its background: contrast is evaluated for the pair. Specify both when you set a coloured treatment.
- Overloading one accent: if the same hue means a link, a warning, a success state and decoration, the interface loses clear cues. Give states explicit semantic roles where they matter.
- Missing state tokens: test hover, focus, active and disabled controls. A polished default button can still have an unreadable hover state.
- Assuming an editorial brand board is a brand standard: every brand-inspired mood board is editorial inspiration only. It is not endorsed by the named brand and is not an official standard. For example, Google and Stripe entries should not replace their organisations’ current identity documentation.
Turn this into a repeatable hand-off
- List the palette’s source swatches as reference tokens.
- Agree a short inventory of semantic roles with design and development.
- Map each role to a reference token for the default theme.
- Check the intended foreground/background pairs, including component states.
- Map the same roles again for dark mode or a campaign theme.
- Use semantic tokens in components, and document exceptions beside the token definition.
For more combinations, browse the Modern SaaS, Minimal & Monochrome and Nature & Organic collections. Before publishing, run the actual foreground/background combinations through the contrast checker or explore all colour tools.
Further reading
- MDN: Using CSS custom properties
- W3C: Understanding WCAG 2.2 Contrast (Minimum)
- Material Design: Design tokens
Related guides
- How to Choose a Colour Palette for a Website
- Website colour accessibility: contrast ratios explained
- How to Use a Five-Colour Palette Without Using Every Colour Equally