Color theory meets practical tooling. Generate harmonious palettes, check WCAG contrast, convert between color spaces, and extract colors from images — all free.
I have a confession: I used to pick colors by gut feeling. Open a color picker, drag around until something looked "nice," drop it into my design, and move on. For years, this worked — or at least I thought it did.
Then a client asked why their call-to-action button was invisible to 8% of their male users. That's when I learned about deuteranopia (red-green color blindness), WCAG contrast ratios, and the fact that "looks nice to me" is not a design methodology.
Color is deceptively complex. It sits at the intersection of physics, perception, psychology, and accessibility. And yet most designers — myself included for an embarrassingly long time — treat it as a vibes-based activity. Pick a brand color, eyeball some complementary shades, hope for the best.
This post is everything I wish I'd known about color tooling when I started. We're going deep: color theory fundamentals, palette generation strategies, contrast checking, color blindness simulation, CSS color functions, dark mode design, and the free tools that make all of it practical.
You don't need a fine arts degree to use color well. You need to understand four harmony types and why they work.
Two colors directly opposite each other on the color wheel. Red and green. Blue and orange. Purple and yellow.
Complementary palettes create maximum contrast. They're loud, energetic, and attention-grabbing. Sports teams love them. Designers reach for them when they need something to pop — a CTA button against a background, a warning badge, a sale banner.
The trap: using complementary colors at equal saturation and equal area. That creates visual vibration — a flickering, headache-inducing effect where both colors fight for dominance. The fix is simple: make one color dominant (60-70% of the area) and the other an accent (10-20%), with neutrals filling the rest.
Three to five colors that sit next to each other on the color wheel. Blue, blue-green, and green. Red, red-orange, and orange.
Analogous palettes feel harmonious and natural. They're easy on the eyes because they share underlying hues. Nature uses analogous schemes constantly — a sunset is reds, oranges, and yellows; a forest is greens, teals, and blue-greens.
The trap: not enough contrast. When everything is the same hue family, nothing stands out. Always pair an analogous palette with a single contrasting accent from the opposite side of the wheel.
Three colors equally spaced around the color wheel (120 degrees apart). Red, yellow, blue. Orange, green, violet.
Triadic palettes are vibrant but balanced. They offer more variety than complementary schemes without the chaos of random picks. Primary triads (red/yellow/blue) feel childish and energetic; secondary triads (orange/green/violet) feel more sophisticated.
The trap: using all three at full saturation. Triadic schemes need one dominant color with the other two muted and used sparingly.
One base color plus the two colors adjacent to its complement. If your base is blue, instead of using orange (the complement), you use red-orange and yellow-orange.
Split-complementary is my go-to scheme and I think it's the most underrated harmony type. It gives you the contrast of a complementary scheme with more nuance and less visual tension. You get warmth and coolness in the same palette without the in-your-face clash.
The trap: treating the two split colors equally. One should be a primary accent; the other should be a subtle supporting shade.
Here's the real-world workflow: a client gives you their brand color — say, #2563EB (a rich blue). Your job is to build an entire palette around it. Not just pick colors that "look nice together," but generate a systematic set of shades, tints, and harmonious accents.
Every base color needs a scale. Not just the base, but 9-11 variants ranging from nearly white to nearly black. If you've used Tailwind CSS, you know this pattern: blue-50, blue-100, blue-200, through blue-900, blue-950.
Building this manually is tedious and error-prone. A good palette generator does it automatically by adjusting lightness while preserving hue and keeping saturation visually consistent. The key word is "visually" — perceptually uniform color spaces like OKLCH handle this much better than HSL, which tends to produce washed-out lights and muddy darks.
From your base blue, a palette generator can produce:
The best palette generators let you tweak the output after generation. Lock one color, adjust another, regenerate the rest. It's a conversation between you and the algorithm, not a one-shot answer.
Color temperature is the single most powerful mood lever you have.
When generating a palette, decide the temperature first. A warm accent in a cool palette creates interest. An entirely warm palette creates energy. An entirely cool palette creates calm. There's no wrong answer, but there are wrong accidents.
Sometimes you don't start with a brand color — you start with a photograph. A hero image, a product shot, a piece of inspiration photography. And you need to extract a usable palette from it.
This is harder than it sounds. A photograph might contain thousands of distinct colors. The question isn't "what colors are in this image?" but "what are the 5-7 most important colors?"
Good extraction tools use clustering algorithms (typically k-means or median cut) to group similar pixels together and identify dominant color regions. The result is a set of representative colors ordered by prominence.
But dominance isn't the same as importance. The sky in a landscape photo is dominant (lots of blue pixels), but the warm golden light on the subject's face might be the color that actually matters for your palette. Better tools offer both dominant-color extraction and accent-color detection.
Crop before extracting. If you want a palette from a product photo, crop out the background first. Otherwise the background color will dominate your palette.
Extract more colors than you need. Pull 8-10 colors, then curate down to 5-6. The extras give you options.
Check contrast ratios immediately. Extracted colors often lack sufficient contrast for text. You'll almost always need to darken or lighten some shades.
Use the photo as a mood board, not a spec sheet. The extracted palette is a starting point. Adjust hues, push saturation, modify lightness until the palette works for UI, not just for matching a photograph.
Let me be blunt: if your design doesn't meet WCAG contrast ratios, it's broken. Not "imperfect" or "could be improved." Broken. For millions of users, low-contrast text is unreadable. And increasingly, it's illegal — the EU's European Accessibility Act took full effect in June 2025, and lawsuits over inaccessible websites are accelerating in the US.
WCAG AA (the minimum you should hit):
WCAG AAA (the gold standard):
Pure black (#000000) on pure white (#FFFFFF) is 21:1 — the maximum possible. Medium gray (#767676) on white is exactly 4.54:1 — barely passing AA for normal text.
The most common contrast failure I see is light gray text on white backgrounds. It looks clean and minimal, and it's completely inaccessible. That beautiful #AAAAAA text on #FFFFFF? That's a 2.32:1 contrast ratio — failing even the lowest WCAG threshold.
The second most common failure is colored text on colored backgrounds without checking. A medium blue on a light blue background might look distinguishable to you, but contrast ratios don't lie. Check them.
The third failure is forgetting about states. Your button has sufficient contrast in its default state, but what about hover? Focus? Disabled? Each state needs its own contrast check.
I've adopted what I call a "contrast-first" approach to palette design:
This order matters. If you design decorative colors first and then try to make them accessible, you'll be fighting your own palette constantly.
Roughly 8% of men and 0.5% of women have some form of color vision deficiency. That's not an edge case — in a room of 25 people, chances are at least one person sees your design very differently than you do.
Never use color alone to convey information. This is WCAG Success Criterion 1.4.1, and it's the single most important color accessibility rule.
Bad: A form field turns red when there's an error. Good: A form field turns red, shows an error icon, and displays error text.
Bad: A chart uses green for positive and red for negative. Good: A chart uses green for positive and red for negative, with different patterns/shapes and text labels.
Color blindness simulation tools let you preview your design through different types of color vision deficiency without guessing. Run your palette through deuteranopia and protanopia simulations at minimum — they cover the vast majority of affected users.
Here's a table I keep bookmarked because I reference it constantly:
| Color Space | Best For | Range/Format |
|---|---|---|
| HEX | CSS shorthand, design specs | #RRGGBB or #RRGGBBAA |
| RGB | Screen display, CSS | rgb(0-255, 0-255, 0-255) |
| HSL | Human-intuitive adjustments | hsl(0-360, 0-100%, 0-100%) |
| OKLCH | Perceptually uniform palettes | oklch(0-1, 0-0.4, 0-360) |
| CMYK | Print design | cmyk(0-100%, 0-100%, 0-100%, 0-100%) |
| HSB/HSV | Design tools (Figma, Sketch) | hsb(0-360, 0-100%, 0-100%) |
For years, HSL was the recommended color space for programmatic palette generation. "Just change the hue and keep saturation and lightness constant!" The problem: HSL's lightness isn't perceptually uniform.
In HSL, yellow at 50% lightness and blue at 50% lightness look completely different in terms of perceived brightness. Yellow appears much lighter than blue, even though the L value is identical. This means HSL-generated palettes have inconsistent visual weight.
OKLCH fixes this. It's a perceptually uniform color space where equal changes in any component produce roughly equal changes in perceived color. If you set two colors to the same L (lightness) in OKLCH, they actually look equally light. This makes it dramatically better for generating consistent palettes.
CSS now supports OKLCH natively:
:root {
--primary: oklch(0.55 0.2 250); /* Blue */
--primary-light: oklch(0.75 0.15 250); /* Same hue, lighter */
--primary-dark: oklch(0.35 0.2 250); /* Same hue, darker */
--accent: oklch(0.55 0.2 30); /* Same lightness, warm hue */
}Notice how the lightness values directly predict perceived brightness. That doesn't work in HSL.
If your palette will be used in print — business cards, brochures, packaging — you need CMYK values. RGB/HEX colors can represent shades that are physically impossible to reproduce with CMYK inks. Neon greens, electric blues, and vivid magentas will look muted in print.
Always convert and preview CMYK versions early if print is involved. Finding out your brand blue looks gray on a business card is not a fun surprise.
CSS has evolved far beyond #hex and rgb(). Modern color functions give you programmatic control over palettes directly in your stylesheets.
Already covered above. The key advantage: perceptually uniform manipulation. Adjust lightness and it looks proportionally lighter. Adjust chroma and it looks proportionally more or less vivid.
.button {
--hue: 250;
background: oklch(0.55 0.2 var(--hue));
}
.button:hover {
background: oklch(0.50 0.22 var(--hue));
}This is the function that made me stop using Sass for color manipulation. color-mix() blends two colors in any color space, right in CSS. No preprocessor needed.
:root {
--brand: #2563eb;
--brand-light: color-mix(in oklch, var(--brand) 40%, white);
--brand-dark: color-mix(in oklch, var(--brand) 60%, black);
--brand-subtle: color-mix(in oklch, var(--brand) 20%, transparent);
}The in oklch part is crucial. Mixing in srgb can produce muddy intermediate colors. Mixing in oklch preserves vibrancy through the blend.
Want to take a color and adjust just its lightness? Or desaturate it? Relative color syntax lets you decompose a color and modify individual channels:
:root {
--brand: oklch(0.55 0.2 250);
--brand-muted: oklch(from var(--brand) l calc(c * 0.5) h);
--brand-shifted: oklch(from var(--brand) l c calc(h + 30));
}This is extraordinarily powerful for theme systems. Define one brand color and derive every variant mathematically.
Every year brings color trends, and most of them are irrelevant to product design. But a few 2026 trends are worth paying attention to because they reflect actual shifts in how people use digital products.
The cold, ultra-minimal aesthetic of 2020-2023 is dying. Users are craving warmth. We're seeing a shift toward warm neutrals (beige, cream, warm gray) as backgrounds, with rich accent colors (terracotta, amber, forest green) replacing the sterile blues and grays.
This isn't just vibes — there's a practical reason. Warm backgrounds reduce perceived eye strain during long reading sessions. As people spend more time in web apps, comfort matters.
The pattern I'm seeing in the strongest 2026 designs: muted, low-saturation base palettes with one or two extremely vivid accent colors. Think a warm gray interface with a single electric indigo button. The contrast between muted and vivid creates focus without visual chaos.
Gradients are back, but they've grown up. Instead of the rainbow vomit gradients of 2018, 2026 gradients are subtle hue shifts within the same color family. A background that shifts from warm beige to soft peach. A card that graduates from slate to soft blue-gray. You almost don't notice them consciously, but they add depth.
Sustainability consciousness is influencing color choices. Olive greens, clay reds, ocean teals, and stone grays are everywhere. These palettes feel grounded, trustworthy, and human — a reaction against the "tech-blue everything" era.
"Just invert the colors" is how every dark mode disaster starts. Dark mode requires a fundamentally different approach to color, not just a lightness swap.
Pure black (#000000) backgrounds look harsh on modern OLED screens and create excessive contrast with white text (21:1 ratio). Most well-designed dark modes use dark grays instead:
| Usage | Light Mode | Dark Mode |
|---|---|---|
| Primary background | #FFFFFF | #121212 to #1A1A2E |
| Elevated surface | #F5F5F5 | #1E1E1E to #252540 |
| Card/container | #FFFFFF | #2A2A2A to #2D2D44 |
| Border | #E5E5E5 | #333333 to #3D3D5C |
In light mode, you create depth with shadows. In dark mode, shadows are invisible on dark backgrounds. Instead, you create depth through lightness: elevated surfaces are slightly lighter than the background. A modal is lighter than the page. A dropdown is lighter than the toolbar.
This is Material Design 3's approach, and it works beautifully. No shadows needed — just subtle lightness differences between surface levels.
Colors that look vibrant on white backgrounds become overwhelming on dark backgrounds. You need to reduce saturation by 10-20% and often increase lightness slightly when moving to dark mode.
:root {
--primary: oklch(0.55 0.2 250); /* Light mode: vivid blue */
}
[data-theme="dark"] {
--primary: oklch(0.65 0.16 250); /* Dark mode: lighter, less saturated */
}Here's the trap: designers check contrast in light mode, ship it, then add dark mode without rechecking. White text on a medium-blue button might pass in light mode but fail when that same button sits on a dark background and the overall visual relationship changes.
Every color combination needs contrast checking in both modes. No exceptions.
A brand color system isn't a palette — it's a structured set of rules about how colors are used. Here's the framework I use:
This ratio creates visual hierarchy naturally. The eye goes to the 10% accent first because it's different from everything else.
Modern design systems define colors by function, not value:
:root {
/* Semantic tokens */
--color-text-primary: var(--gray-900);
--color-text-secondary: var(--gray-600);
--color-text-link: var(--blue-600);
--color-bg-page: var(--white);
--color-bg-surface: var(--gray-50);
--color-bg-interactive: var(--blue-600);
--color-border-default: var(--gray-200);
--color-feedback-error: var(--red-600);
--color-feedback-success: var(--green-600);
--color-feedback-warning: var(--amber-500);
}When you switch themes (light/dark, brand variations), you swap the semantic tokens' values. The component code never changes. This is how every serious design system works in 2026.
A brand color system without documentation is just a collection of hex codes. Document:
Linear gradients between two colors are fine, but the interesting work happens with multi-stop gradients, mesh gradients, and gradient maps.
The trick to smooth multi-stop gradients is using oklch interpolation in CSS:
.hero {
background: linear-gradient(
in oklch 135deg,
oklch(0.7 0.15 330), /* Rose */
oklch(0.6 0.2 280), /* Purple */
oklch(0.55 0.2 250) /* Blue */
);
}The in oklch keyword tells the browser to interpolate through perceptually uniform color space. Without it, you get muddy midpoints when transitioning between distant hues. With it, the intermediate colors are vivid and natural.
Mesh gradients use multiple color points positioned in 2D space, creating organic, blob-like color transitions. They've become a defining visual element of modern SaaS landing pages.
CSS doesn't natively support mesh gradients yet, but you can approximate them by layering multiple radial gradients:
.mesh-bg {
background:
radial-gradient(ellipse at 20% 50%, oklch(0.8 0.1 250 / 0.6), transparent 50%),
radial-gradient(ellipse at 80% 20%, oklch(0.85 0.12 330 / 0.5), transparent 50%),
radial-gradient(ellipse at 60% 80%, oklch(0.75 0.1 180 / 0.4), transparent 50%),
oklch(0.95 0.02 250);
}Gradients can make text unreadable. A gradient background that passes contrast at one end may fail at the other. Always check contrast against the lightest point in the gradient that the text overlaps. Better yet, put text on solid or semi-transparent overlays instead of directly on gradients.
"What color is #6D28D9?" If you said "purple" or "violet," you're right. But in a codebase with 50 color variables, names like purple-700 or violet-600 quickly become ambiguous.
There are two schools:
Descriptive (Tailwind approach): red-500, blue-600, emerald-400. You know the color at a glance. But changing your brand from blue to purple means renaming every variable.
Functional (token approach): primary-500, accent-600, surface-200. You know the purpose at a glance. Changing brand colors means updating one mapping, not 200 references.
Most mature design systems use both: descriptive names as a primitive layer and functional names as the public API.
CSS has 148 named colors (from aliceblue to yellowgreen), and I'll be honest — most of them are useless for production work. But they're handy for prototyping and for those rare moments when tomato is exactly the shade you want.
Alpha transparency is one of the most powerful and misunderstood color capabilities.
CSS now supports alpha in all modern color functions:
.overlay {
/* Modern: space-separated with / alpha */
background: rgb(0 0 0 / 0.5);
background: hsl(220 80% 50% / 0.3);
background: oklch(0.5 0.2 250 / 0.8);
}The old rgba() and hsla() functions still work but are considered legacy syntax.
Alpha transparency enables several powerful patterns:
background: oklch(1 0 0 / 0.1); backdrop-filter: blur(20px);border: 2px solid oklch(0.5 0.2 250 / 0.5); background: transparent;Semi-transparent colors don't stack linearly. Two layers of rgba(0,0,0,0.5) don't equal rgba(0,0,0,1.0). The result is rgba(0,0,0,0.75) — because the second layer's 50% opacity applies to the remaining 50% of light passing through the first layer.
This matters when you're building layered interfaces. Always check the composed result, not just individual layer values.
Here's how different color tools stack up for real design work:
| Capability | Basic Picker | Palette Generator | Pro Design Tool | Browser-Based Suite |
|---|---|---|---|---|
| HEX/RGB/HSL conversion | Yes | Yes | Yes | Yes |
| OKLCH support | Rare | Sometimes | Yes | Varies |
| Harmony generation | No | Yes | Limited | Yes |
| Image palette extraction | No | Sometimes | Yes | Yes |
| WCAG contrast checking | No | Sometimes | Plugin | Yes |
| Color blindness simulation | No | No | Plugin | Sometimes |
| CSS code export | Limited | Yes | No | Yes |
| CMYK conversion | No | No | Yes | Yes |
| Dark mode preview | No | No | Yes | Sometimes |
| Gradient generation | No | Sometimes | Yes | Yes |
The sweet spot for most designers is a browser-based suite that combines multiple tools: a picker, converter, palette generator, contrast checker, and gradient builder in one place. Jumping between five different websites for five different color tasks is a workflow killer.
Here's my actual process when building a color system for a new project:
Step 1: Define the brand color. Get the client's primary color (or choose one). Convert it to OKLCH so all subsequent calculations are perceptually uniform.
Step 2: Generate the shade ladder. Create 10-12 shades from near-white to near-black. Check that each adjacent pair has distinguishable contrast.
Step 3: Generate harmonic accents. Use split-complementary harmony to create 2-3 accent colors. Generate shade ladders for each.
Step 4: Define semantic tokens. Map shade values to functions: text, background, border, interactive, feedback.
Step 5: Check contrast everywhere. Every text/background combination. Every interactive element. Both light and dark modes.
Step 6: Simulate color blindness. Run the full palette through deuteranopia and protanopia simulations. Fix any combinations that become indistinguishable.
Step 7: Export everything. CSS custom properties, design tokens JSON, and a reference sheet with all values.
This process takes about 30-45 minutes with good tools. Without them, it takes hours and you'll miss accessibility issues.
Everything I've described in this post — palette generation, contrast checking, color space conversion, color blindness simulation, gradient creation — is available for free in browser-based tools.
I use a suite of color tools that handles the full workflow: pick a color, convert between formats, generate harmonious palettes, check WCAG contrast ratios, and export production-ready CSS. The converter tools handle HEX, RGB, HSL, CMYK, and the newer color spaces. The contrast checker gives instant AA/AAA pass/fail status. The palette generator supports all the harmony types I described above.
For image-based work, the photo editor includes color extraction from images — drag in a photo, pull out the dominant palette, and start designing from there.
No signups. No watermarks. No "upgrade to unlock." Just the tools, in your browser, doing the job.
The biggest lie in design is that "some people just have an eye for color." Color harmony isn't magic — it's geometry. Complementary means 180 degrees. Triadic means 120 degrees. Split-complementary means 150 and 210 degrees. These are spatial relationships on a wheel, not mystical abilities.
Accessibility isn't subjective either. A contrast ratio of 4.5:1 passes AA. Below that, it fails. A color combination that's distinguishable under deuteranopia simulation works for color-blind users. One that collapses to identical shades doesn't.
The tools to do all of this are free, fast, and browser-based. The theory is well-documented. The standards are clear.
The only thing standing between a mediocre palette and a great one is twenty minutes of systematic work. Pick your base color. Generate your harmonies. Check your contrast. Simulate color blindness. Export your tokens.
Then go build something beautiful — and accessible.