Build a CSS Design System from an Image Palette
By ImgScale Team · June 2026 · 7 min read · by kizura
Extracting a shade palette from an image is step one. Turning those swatches into a real, usable design system — with semantic names, contrast-checked text colors, and a dark mode variant — is step two. This tutorial walks through the complete process.
Open the Palette ExtractorStep 1: Extract the palette
Open the Palette Extractor and drop in your brand image — a product photo, a mood board, a hero image, or a reference photo that captures the feel you want.
- Extract 6–8 colors for most design systems. More than 8 is usually noise.
- Sort by Dominance first to see which colors matter most.
- Switch to Lightness sort to check you have a spread from light to dark. A good palette needs at least one very light, one very dark, and a few mid-tones.
- Download the palette strip PNG for reference as you work.
Then select the HEX export format and copy the list. You'll use these as raw values in the next step.
Step 2: Name your colors semantically
Raw hex codes like #1B2A6B are not usable in a design system. Rename each shade based on what it is, not what it looks like. Two approaches:
Descriptive names (literal)
--color-midnight: #1B2A6B; /* deep blue */
--color-plum: #9A5FB0; /* purple */
--color-amber: #F4A259; /* orange-yellow */
--color-forest: #2C6E49; /* dark green */
--color-sand: #FFD98A; /* light yellow */
--color-cream: #ECE7DF; /* off-white */
Numeric scale (systematic)
--color-100: #ECE7DF; /* lightest */
--color-200: #FFD98A;
--color-300: #F4A259;
--color-400: #9A5FB0;
--color-500: #2C6E49;
--color-900: #1B2A6B; /* darkest */
Descriptive names are easier to use intuitively. Numeric scales are better for systematic design tokens. For most small projects, descriptive names win.
Step 3: Assign design roles
On top of the named colors, create a layer of semantic role variables:
:root {
/* Raw palette */
--c-midnight: #1B2A6B;
--c-plum: #9A5FB0;
--c-amber: #F4A259;
--c-forest: #2C6E49;
--c-sand: #FFD98A;
--c-cream: #ECE7DF;
/* Semantic roles */
--color-primary: var(--c-forest); /* main CTA, links */
--color-secondary: var(--c-plum); /* accents, badges */
--color-accent: var(--c-amber); /* highlights, warnings */
--color-bg: var(--c-cream); /* page background */
--color-surface: #fff; /* card backgrounds */
--color-text: var(--c-midnight); /* body text */
--color-text-muted: var(--c-plum); /* secondary text */
--color-border: rgba(27,42,107,.15); /* borders */
}
Using two layers means you can change the entire theme by only editing the raw palette section — the semantic roles automatically follow.
Step 4: Check contrast ratios
WCAG 2.1 requires a contrast ratio of at least 4.5:1 for body text and 3:1 for large text and UI components. Before shipping, check that your text/background combinations meet these thresholds.
The most important combinations to check:
--color-texton--color-bg(body text)--color-primaryon--color-bg(link color)- White text on
--color-primary(CTA button) --color-text-mutedon--color-bg(secondary text — often the one that fails)
Use a contrast checker tool (WebAIM's is free) or the DevTools color picker. If a combination fails, adjust the lightness of one color until it passes — don't use a failing combination even if it "looks fine" on your screen.
Common failure: Muted text colors extracted from images often look fine visually but fail WCAG at 3:1 or even 2:1. Always check numerically, not just visually.
Step 5: Write the complete CSS
:root {
/* ── Palette ── */
--c-midnight: #1B2A6B;
--c-plum: #9A5FB0;
--c-amber: #F4A259;
--c-forest: #2C6E49;
--c-sand: #FFD98A;
--c-cream: #ECE7DF;
/* ── Semantic ── */
--color-bg: var(--c-cream);
--color-surface: #FFFFFF;
--color-text: var(--c-midnight);
--color-text-muted: #5A5370; /* darkened plum for contrast */
--color-primary: var(--c-forest);
--color-primary-text: #FFFFFF;
--color-secondary: var(--c-plum);
--color-accent: var(--c-amber);
--color-border: rgba(27,42,107,.12);
}
body {
background: var(--color-bg);
color: var(--color-text);
}
a { color: var(--color-primary); }
button.primary {
background: var(--color-primary);
color: var(--color-primary-text);
}
Step 6: Add dark mode
A dark mode variant doesn't need entirely new colors — just reassign the roles for dark context:
@media (prefers-color-scheme: dark) {
:root {
--color-bg: var(--c-midnight);
--color-surface: #242D5C; /* slightly lighter midnight */
--color-text: var(--c-cream);
--color-text-muted: var(--c-sand);
--color-primary: var(--c-amber); /* amber pops on dark bg */
--color-primary-text: var(--c-midnight);
--color-border: rgba(236,231,223,.12);
}
}
Notice that --color-primary changed from forest green to amber in dark mode — green can look harsh on dark backgrounds, while amber is warm and readable. The two-layer variable system makes this swap a one-liner.
Using with Tailwind instead
Select the Tailwind export format in the Palette Extractor to get a config-ready object. Rename the keys to semantic names before adding to your config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
midnight: '#1B2A6B',
plum: '#9A5FB0',
amber: '#F4A259',
forest: '#2C6E49',
sand: '#FFD98A',
cream: '#ECE7DF',
}
}
}
}
Then use bg-cream, text-midnight, border-forest throughout your templates. Tailwind's JIT engine will only include the classes you actually use.
Related: How Color Palette Extraction Works · PNG vs JPG vs WEBP Guide
Naming and organizing your tokens
Once you have a set of values, give each one a clear, semantic name rather than a literal one. A token called --surface or --accent describes its role and survives a redesign, whereas a literal name tied to a specific hue becomes misleading the moment you adjust the palette. Group tokens by purpose — backgrounds, text, borders, interactive states — so the system stays readable as it grows. This naming discipline is what turns a flat list of swatches into a maintainable design system that a whole team can use consistently.
Finally, check contrast. Any combination used for text against a background should meet accessibility ratios so the result is legible for everyone, including users with low vision. Tools that report contrast ratios make this quick, and building accessibility in from the start is far easier than retrofitting it later.