Designing a Color System That Scales
A handful of brand colors is not a color system. A system survives new themes, dark mode, and accessibility audits because it is built on roles and ramps, not on a pile of favorite hex values.
Most products start with color the same way: someone picks a brand blue, an accent, a few grays, and a green and red for success and error. It works until it doesn't — until dark mode arrives, or an accessibility audit fails, or a second brand needs supporting, and the handful of hand-picked values turns out to have no underlying structure to extend. A color system is what you build so that growth does not mean starting over.
From a pile of hex to a system
The difference between a palette and a system is structure. A palette is a list of colors someone liked. A system is a set of rules that generate and govern color: how many steps each hue has, what each step is for, which colors may sit on which, and how the whole thing adapts when the context changes. The colors are the output; the rules are the system.
Structure is what lets a system answer questions a palette cannot. What is the hover state of this button? Which gray is correct for a disabled label on a card? What does this color become in dark mode? A pile of hex values shrugs at these questions. A system has an answer because the answer was designed in, not picked each time.
Roles, not just colors
The first shift is to stop thinking in colors and start thinking in roles. The UI does not need "blue"; it needs an accent, a surface, a border, a primary text color, a muted text color. Each role describes a job, and a job can be filled by different actual colors in different themes. Designing around roles is what makes a system portable across themes and brands.
Roles also force clarity about contrast relationships. "Text on surface" is a pairing with a job to do: it must remain legible. When you design the role rather than the color, you are designing the relationship, and the relationship is what users actually experience.
Building tints and shades
Each hue in a system is usually expressed as a ramp — a sequence of steps from very light to very dark, typically labeled 50 through 900. A single brand blue becomes a family: light tints for backgrounds and subtle fills, mid tones for the color itself, dark shades for text and hover states. The ramp is what gives you somewhere to go when you need "the same blue, but slightly darker for the pressed state."
:root {
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-300: #93c5fd;
--blue-500: #3b82f6; /* the base brand blue */
--blue-600: #2563eb; /* hover / pressed */
--blue-700: #1d4ed8;
--blue-900: #1e3a8a; /* text on light */
}Building a good ramp is more than lightening and darkening. Naively adjusting lightness tends to wash out or muddy the mid and dark steps, because perceived lightness and hue do not move linearly. Many teams now build ramps in a perceptual color space so each step looks evenly spaced to the eye, which is the only spacing that matters.
Contrast and accessibility
Color is not decoration; it is information, and information that some people cannot perceive is information you have not really conveyed. Contrast is the core accessibility concern: text must stand clearly against its background. The WCAG guidelines give concrete ratios to aim for — a higher bar for small body text, a lower one for large text and interface elements.
- Check text-on-surface pairings against the WCAG contrast ratios, not by eye alone.
- Never use color as the only signal — pair it with text, icons, or shape, so meaning survives color-blindness.
- Bake contrast into the ramp: define which steps are safe for text on which backgrounds.
- Re-check every pairing in dark mode; contrast that passes on white can fail on near-black.
Designing contrast into the ramp from the start is far cheaper than retrofitting it. If step 700 is the designated text-on-light color and step 100 the designated light surface, the safe pairings are decided once and reused, instead of relitigated for every screen.
A color that some users cannot distinguish is not a design choice. It is information you forgot to send.
Semantic color tokens
The ramps are the raw material; semantic tokens are how the UI consumes them. Rather than letting a component reference blue-600 directly, you define color-accent and point it at blue-600. The component asks for the accent, and the system decides what the accent currently is. This indirection is what turns ramps into a system that can be re-themed without touching components.
Keep the semantic layer small and role-based: text, text-muted, surface, surface-raised, border, accent, and a handful of status colors. A lean semantic vocabulary is easier to hold in your head and harder to misuse than a sprawling one with a token for every conceivable situation.
Theming and dark mode
With roles, ramps, and semantic tokens in place, dark mode becomes almost mechanical. You are not redesigning anything; you are remapping the semantic tokens to different steps of the same ramps. Surface moves from a light step to a dark one, text inverts, borders soften. Because components only ever asked for roles, they follow along without changes.
The same machinery supports more than dark mode. A high-contrast theme, a per-customer brand, a print style — each is just another mapping of the semantic layer onto the ramps. The work you did to make dark mode possible is the work that makes every future theme cheap.
Building yours
Start with roles, not swatches. List the jobs your interface needs color to do, build a small ramp for each hue, decide the safe contrast pairings, and wire it all together with a thin layer of semantic tokens. It is more upfront thought than picking five favorite colors — but it is the difference between a palette that has to be rebuilt at every turn and a system that quietly absorbs whatever the product grows into.



