Picking colours is the short part. Getting them into a project in a form that stays maintainable is where palettes usually go wrong, and the format you export in largely decides how painful the next rebrand will be.
The formats
CSS custom properties. The default choice for most projects. Define them once on :root,
reference them everywhere, and they update live — including inside media queries, which is
what makes dark mode straightforward.
:root {
--blue-500: #3d8bf5;
--color-text-primary: var(--blue-800);
}
Sass variables. Resolved at build time, so they cannot change at runtime and cannot power a theme switch on their own. Still useful for values that genuinely never vary, and for computing a palette at build time.
Tailwind config. Extends the default theme so your scale becomes available as utility classes. Worth keeping the numbered-step convention so it matches Tailwind’s own scales.
Design tokens (JSON). The interchange format when the same palette has to reach web, iOS, Android and design tools. More setup, and the only approach that genuinely keeps multiple platforms in step.
SVG and image formats for swatch sheets, and plain lists when you just need to paste values into a document.
Two layers of naming
The single most useful thing you can do at export time is separate what a colour is from what it is for:
:root {
/* Layer 1 — the palette. Descriptive, never used directly in components. */
--blue-600: #2b6cb0;
--grey-900: #1a202c;
/* Layer 2 — semantic. This is what components reference. */
--color-action: var(--blue-600);
--color-text-primary: var(--grey-900);
}
Components reference only the second layer. Rebranding then means repointing a handful of
semantic tokens rather than finding every use of --blue-600 — and dark mode becomes a matter
of redefining layer two under a media query while layer one stays put.
Dark mode
Two approaches. Either redefine the semantic tokens inside
@media (prefers-color-scheme: dark), or define both sets and switch with a data-theme
attribute on the root element. The attribute approach is more code but supports an explicit
user toggle, which most people eventually want. Whichever you choose, remember that a scale
does not simply invert — contrast behaves differently on dark backgrounds and saturated
colours read as brighter. Recheck the pairs with the
contrast checker.
Where people go wrong
Exporting hex when you generated in OKLCH. Hex is fine as output, but keep the OKLCH values in source somewhere. Regenerating a scale from hex means redoing the perceptual work.
Naming by appearance. --light-blue has nowhere to go when you need something between it
and --blue. Number by lightness; the
colour scale generator covers the convention.
Skipping the semantic layer on small projects. It costs almost nothing up front and saves a great deal later. Small projects grow.
Committing exported colours without checking them in context. A palette that looks right in a swatch grid can be wrong in a layout, where areas differ wildly in size. Large surfaces need quieter colours than small ones.
Letting the palette drift. Once colours live in code, people add “just one more” shade inline. Periodically diffing what is used against what is defined is worth the hour.
Our guide on colour scales for design systems covers structuring the palette before you export it.