DEV Community

Mayank Chawdhari
Mayank Chawdhari

Posted on

What developers always miss when building color schemes (and how to fix it)

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, and on (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

  1. 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-bg references --color-primary.
    1. Use HSL (or CSS color functions): tunable — changing lightness or saturation produces consistent tints/shades.
    2. Provide states: for each semantic color include base, on (text), hover, active, disabled.
    3. Prefer off-white / off-black: #fff and #000 are harsh. Slightly off values reduce eye strain.
    4. Test contrast for real text sizes (WCAG AA as minimum for body text).
    5. Don’t rely on color alone for statuses — add icons, text, or patterns.
    6. Design dark mode independently — adjust saturation and lightness; don’t blindly invert.
    7. 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; }
Enter fullscreen mode Exit fullscreen mode

Quick tips & practical patterns

  • Elevation with alpha: use subtle alpha overlays (e.g., background: rgba() / color-mix() or linear-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/#fff everywhere 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;
}
Enter fullscreen mode Exit fullscreen mode

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)