Color is not decoration. It’s hierarchy, affordance, accessibility, and mood — all encoded in tiny hexes. Ship a color system, not a handful of random swatches. Below is a ready-to-publish Dev.to post in Markdown you can copy, paste, and publish as-is.
Stop treating color like an afterthought — a practical guide to color systems for devs
Too many apps pick a primary color, slap it on buttons, and call it a day. The result: inconsistent UIs, accessibility failures, and a product that feels half-baked. This post lays out the subtle—but high-impact—things developers routinely forget when designing color systems, plus practical rules, copy-paste tokens, and an accessibility checklist you can run before launch.
What most devs miss (quick list)
- Semantic tokens, not “random hexes” — colors should represent roles (background, surface, primary, success, border), not only components.
- Contrast & accessibility — color choices must work for low-vision and color-blind users, not just look pretty on a monitor.
-
State & scale — every semantic color needs
base,hover,active,disabled, andon(text-on-color) variants. - Color-only meaning — never rely on color alone to convey status; pair with iconography, labels, or patterns.
- Dark mode parity — dark theme is a first-class citizen; don’t just invert light values.
- Saturation & chroma control — tame saturation for large surfaces; reserve high chroma for CTAs and micro-interactions.
-
Token naming & design tokens — name by role (
--color-surface) not by hue (--blue-500). - Device gamut awareness — wide-gamut displays (P3) make colors pop; design defensively so things don’t blow out.
- Visible focus & keyboard affordances — ensure focus rings remain legible on all backgrounds.
- Testing & tooling — integrate contrast checks and color-blind simulators into QA.
Concrete rules — how to build a reliable color system
- Layer your tokens
- Foundational tokens: hue/saturation/lightness values for brand and feedback colors.
-
Semantic tokens:
--color-surface,--color-primary,--color-border. -
Component tokens:
--btn-primary-bgreferences--color-primary.- Use HSL (or CSS color functions): tunable — changing lightness or saturation produces consistent tints/shades.
-
Provide states: for each semantic color include
base,on(text),hover,active,disabled. -
Prefer off-white / off-black:
#fffand#000are harsh. Slightly off values reduce eye strain. - Test contrast for real text sizes (WCAG AA as minimum for body text).
- Don’t rely on color alone for statuses — add icons, text, or patterns.
- Design dark mode independently — adjust saturation and lightness; don’t blindly invert.
- Make tokens available to designers and devs (JSON/CSS exports) so both sides use the same source of truth.
Copy-paste CSS starter (semantic tokens + states)
/* 1) Base HSL tokens (role-based, tuneable) */
:root{
/* Brand / primary in HSL */
--primary-h: 210;
--primary-s: 88%;
--primary-l: 52%;
/* Feedback hues */
--success-h: 140;
--success-s: 55%;
--success-l: 38%;
--error-h: 12;
--error-s: 84%;
--error-l: 48%;
/* Neutral scale (store as H S% L% strings when helpful) */
--neutral-0: 0 0% 100%; /* white-ish surface */
--neutral-1: 220 14% 96%;
--neutral-2: 220 12% 88%;
--neutral-3: 220 10% 72%;
--neutral-4: 220 8% 44%; /* text default */
--neutral-5: 220 6% 10%; /* near black */
}
/* 2) Derived semantic tokens */
:root{
--color-surface: hsl(var(--neutral-1));
--color-surface-2: hsl(var(--neutral-2));
--color-text: hsl(var(--neutral-4));
--color-text-strong: hsl(var(--neutral-5));
--color-primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
--color-primary-hover: hsl(var(--primary-h), var(--primary-s), calc(var(--primary-l) - 8%));
--color-primary-active: hsl(var(--primary-h), var(--primary-s), calc(var(--primary-l) - 14%));
--color-primary-on: white;
--color-success: hsl(var(--success-h), var(--success-s), var(--success-l));
--color-error: hsl(var(--error-h), var(--error-s), var(--error-l));
--color-border: hsl(220 10% 85%);
}
/* 3) Component usage example */
.btn {
background: var(--color-primary);
color: var(--color-primary-on);
padding: .6rem 1rem;
border-radius: 8px;
border: 1px solid transparent;
transition: background .12s ease, transform .06s ease;
}
.btn:hover { background: var(--color-primary-hover); transform: translateY(-1px); }
.btn:active { background: var(--color-primary-active); transform: translateY(0); }
.btn[disabled] { opacity: .5; cursor: not-allowed; }
Quick tips & practical patterns
-
Elevation with alpha: use subtle alpha overlays (e.g.,
background: rgba()/color-mix()orlinear-gradient) rather than many solid surface colors. - Reserve saturation for action: low saturation on large surfaces; higher saturation for CTAs and micro-interactions.
- Icon color = text color — treat icons as text for accessibility/contrast.
-
Token naming rule:
--color-<role>(e.g.,--color-surface,--color-border,--color-primary) — makes intent obvious. - Design tokens pipeline: export tokens to JSON/CSS and import into Figma/Sketch so design and code stay in sync.
- Dark mode strategy: pick a neutral dark background (not pure black), lower saturation of primaries, raise contrast for text.
Small but high-impact gotchas developers skip
- Disabled states without contrast tests (users can’t read disabled labels).
- Tiny icons with insufficient contrast (icons are UI text too).
- Relying on red/green alone for status—fails for color-blind users.
- Focus rings that disappear on colored backgrounds.
- Using
#000/#fffeverywhere instead of pleasant off-black/off-white for better perceived contrast.
Example: focus-visible and accessible error
/* visible focus */
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--color-primary) 80%, white 20%);
outline-offset: 3px;
}
/* error state with icon + label */
.input--error {
border-color: var(--color-error);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 8%, transparent);
}
.error-msg {
display: flex;
gap: .5rem;
align-items: center;
color: var(--color-error);
font-size: .9rem;
}
Conclusion
Color is not an afterthought — it’s a system. Treat it like one: define semantic tokens, plan states (hover/active/disabled/on), and design dark mode intentionally. Make accessibility (contrast, color-blind checks, visible focus) a non-negotiable part of your workflow, not an optional QA step. Small, deliberate color decisions — saturation, context, and naming — are what separate a hurried UI from a polished, inclusive product. Ship the system, not just a swatch.
Top comments (0)