The single-color starting point
Every design system starts the same way: someone picks a color. Maybe it's from a logo, a mood board, or a client's email that says "we like this blue." You have one hex code and a blank canvas.
The gap between that single color and a production-ready system — with semantic roles, dark mode support, accessible pairings, and scalable structure — is where most projects stall. Not because it's hard, but because the steps aren't obvious.
Here's the exact workflow.
Step 1: Convert to OKLCH and understand what you have
Take your hex and convert it to OKLCH. This gives you three values that actually mean something:
/* Starting color: #4F46E5 (indigo) */
oklch(0.49 0.22 264)
L = 0.49 → mid-range lightness (slightly dark)
C = 0.22 → high chroma (vivid, saturated)
H = 264 → blue-violet hue These values tell you the character of your brand color. High chroma means it's bold and attention-grabbing. Mid lightness means it works well as a primary action color but needs lighter and darker variants. The hue places it in the cool spectrum.
Step 2: Generate a lightness scale
A single color isn't a system. You need a full scale — typically 11 steps from near-white to near-black — that lets you reach for the right weight in any context.
In OKLCH, this is straightforward: keep the hue constant, keep chroma proportional, and step evenly through lightness:
--primary-50: oklch(0.97 0.03 264); /* tinted background */
--primary-100: oklch(0.93 0.05 264); /* hover background */
--primary-200: oklch(0.87 0.08 264); /* subtle highlight */
--primary-300: oklch(0.78 0.12 264); /* border, badge bg */
--primary-400: oklch(0.67 0.17 264); /* muted text on dark */
--primary-500: oklch(0.49 0.22 264); /* ← your brand color */
--primary-600: oklch(0.43 0.20 264); /* hover state */
--primary-700: oklch(0.37 0.18 264); /* pressed state */
--primary-800: oklch(0.29 0.14 264); /* dark text */
--primary-900: oklch(0.22 0.10 264); /* heading on light */
--primary-950: oklch(0.15 0.06 264); /* near-black */ Notice how chroma decreases at the extremes. Very light and very dark colors can't hold high chroma — they'd look neon or muddy. Tapering chroma naturally is what keeps the scale looking professional.
Step 3: Define semantic roles
Raw scales are tools. Semantic roles are the interface your components actually use. Map your scale values to roles:
- Primary — Your brand color (500). Buttons, links, focus rings.
- Primary Hover — One step darker (600). Interactive feedback.
- Primary Subtle — Very light tint (50 or 100). Badge backgrounds, selected row highlights.
- Background — Neutral, near-white. Usually not from your brand scale — use a neutral with a slight hue tint.
- Surface — Cards, panels. Pure white or a very subtle tint.
- Text — Near-black. Can be from your brand's 900/950 for a cohesive feel, or a pure neutral for maximum readability.
- Muted — Secondary text, timestamps, placeholders. Gray with optional brand tint.
- Border — Dividers and outlines. Light gray, slightly warmer than pure gray.
Step 4: Add a complementary accent
One color isn't enough for a real interface. You need at least one accent color for secondary actions, success states, or visual variety. The fastest way to find one that works:
- Complementary — Rotate hue 180°. For H=264, that's H=84 (a warm yellow-green). High contrast, high energy.
- Analogous — Rotate hue 30°. For H=264, that's H=294 (purple) or H=234 (blue). Harmonious, low tension.
- Split complementary — Rotate hue ±150°. Gives two accent options that both contrast with the primary without being as intense as a direct complement.
Pick one, generate its own lightness scale, and assign it to accent semantic roles. For most products, one primary + one accent covers 90% of use cases.
Step 5: Build the dark mode variant
With OKLCH, dark mode is a systematic transformation, not a redesign:
- Backgrounds — Flip to near-black. Use your brand hue at very low chroma for tinting:
oklch(0.13 0.01 264). - Surfaces — Slightly lighter than background:
oklch(0.16 0.01 264). - Text — Flip to near-white:
oklch(0.93 0.01 264). - Primary — Increase lightness slightly, decrease chroma slightly. The color should be recognizable but less intense.
- Borders — Very subtle light edges:
oklch(0.22 0.01 264).
The rule of thumb: in dark mode, lightness values roughly mirror around 0.50, and chroma drops by 15–25%.
Step 6: Verify contrast ratios
Before you ship, check every text-on-background pair:
- Text on background — must pass 4.5:1 (AA)
- Muted text on surface — must pass 4.5:1
- Primary on primary-subtle — check this; it often fails
- All of the above in dark mode too
If a pair fails, adjust the lighter color's lightness up or the darker one's down. In OKLCH, you know exactly which direction to move and by how much.
Step 7: Export and ship
Package the result as CSS custom properties, a Tailwind config, or an AI rules file. The format depends on your stack, but the structure is the same: semantic names mapping to OKLCH values, with light and dark variants.
On kuler.ai, this entire workflow — from a starting palette to a rules file — is a few clicks. Pick a palette, export for your tool, and you have a complete color context file that your AI coding tool, design system, or component library can consume immediately.
A design system doesn't need a hundred colors. It needs six well-chosen ones, systematically expanded, and rigorously verified.