The difference between a palette that stays maintainable and one that becomes a nightmare is usually a single decision: whether the colors live in one place or are scattered across hundreds of rules. CSS custom properties put them in one place, and give you theming, contrast safety and painless rebrands as a side effect.
Key takeaways
- Declare the generated palette once on
:rootso every element can reach it. - Use two layers: raw color tokens, then semantic role tokens that components reference.
- Theming becomes an override of roles — components never change.
- Store each background's safe text color beside it so accessibility lives in the tokens.
Define the palette once, at the root
Declare your generated palette as custom properties on the :root selector so every element in the document can reach them:
:root {
--plum: #8B2D55;
--blush: #FFA8C2;
--ink: #28151F;
--cream: #FFF9FB;
--deep: #6E2143;
}
The generator's Export CSS button produces exactly this block, so a palette moves from tool to codebase without anyone retyping a hex digit — the single most common source of a color that is almost, but not quite, correct.
Name by role, not by appearance
Resist naming variables after the color itself in the layer components use. --plum is fine as a raw token, but a second, semantic layer is what keeps the system flexible:
:root {
--color-surface: var(--cream);
--color-text: var(--ink);
--color-primary: var(--plum);
--color-accent: var(--blush);
}
Now if the brand pink changes, you update one raw token and every role pointing at it follows. Without that layer, a rebrand means finding every rule that says --plum and deciding, one by one, whether it meant "the brand color" or "this particular pink".
Two layers — raw color tokens and semantic role tokens — is the pattern that lets a design system survive a rebrand without touching component code.
Theme by overriding, not rewriting
Because components reference roles, an entire alternate theme is just a redefinition of those roles under a class or attribute:
[data-theme="dark"] {
--color-surface: var(--ink);
--color-text: var(--cream);
}
Nothing in your buttons or cards changes. They still refer to --color-surface, which now resolves to a different value. This is the mechanism behind a clean dark mode, and it is why theme switching can be a single attribute toggle rather than a second stylesheet.
Keep contrast in the tokens
Because each role has a defined text pairing, bake accessibility into the token layer rather than leaving it to whoever builds the next component. If --color-primary needs white text on top, define --color-on-primary right beside it, verified once in the contrast checker. Components then never have to guess which text color is safe, and an accessible result becomes the path of least resistance.
Where the payoff shows up
With this structure in place, re-theming an entire site is a handful of lines, a new palette from the generator drops in cleanly, and no component hard-codes a color again. The same role-based thinking transfers to non-web targets too — the mapping onto Flutter themes is almost one to one.
Fallbacks, scoping and the pitfalls
Two details catch people out. First, custom properties inherit, which is a feature: define a role on a component's wrapper and everything inside picks it up, letting a card carry its own surface color without a single override. Second, they resolve at use time, so an invalid or missing value falls back to the second argument of var() — always supply one for tokens that might not exist in every context. Avoid the temptation to compute colors inside the token layer with opaque functions; a token whose value cannot be read at a glance is a token nobody trusts. Keep the raw layer literal, the semantic layer aliased, and the arithmetic in the build step.
Frequently asked questions
Should I use CSS custom properties or Sass variables?
Custom properties, wherever theming or runtime changes matter — they are live values the browser can swap. Sass variables are compile-time and cannot respond to a theme toggle.
How many color tokens does a project need?
Usually five to eight raw colors and eight to fifteen semantic roles. If the role list keeps growing, some roles are describing components rather than jobs.
Can custom properties hold whole gradients or shadows?
Yes. Any valid CSS value works, so gradients, shadows and border colors all belong in the same token layer as your palette.
For the Sass and Tailwind versions of this same source of truth, see the guide to putting a palette into CSS and Tailwind.