#3C0D4F |
#F22C33 |
#C6EEA0 |
#1681DF |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
🎨 Palette in play — Mango Garden Moment ·
#3C0D4F#F22C33#C6EEA0#1681DF
Picking colors is the part of a side project where an evening quietly disappears. You open a generator, spin the wheel forty times, save three screenshots, and end up shipping the same slate-and-indigo you always ship.
The problem is not taste. It is that a palette is chosen as a picture and used as a system. Four squares side by side always look balanced. The same four colors, applied to a heading, a body paragraph, a disabled button and a focus ring, very often do not.
Here is the process I use to go from "ooh, that one" to a theme that survives code review.
🧩 1 · Read the structure before the colors
Ignore whether you like it for a second and ask: what shape is this palette? Almost every four-color set falls into one of four structures, and the structure tells you what it can be used for.
| Structure | Ships well as | Watch out for | ||||
|---|---|---|---|---|---|---|
| Ramp — one hue, four lightnesses | a whole dashboard: bg, borders, muted text, text | no accent — you must add one | ||||
| Base + signals — one dark, three brights | dark mode with status colours | brights are unreadable on white | ||||
| Lights + anchor — three pales, one deep | light UI with one strong CTA | only one usable text colour | ||||
| Near-monochrome — four neighbours | backgrounds, illustration, packaging | no text can live here |
That last row is the classic trap. The lightest and darkest of those four honeys are 1.45:1 apart. It is gorgeous, and it is a set of surfaces — bring your own ink or nobody can read your app.
🔬 2 · Test the pairs you will actually ship
A palette is not four colors; it is up to twelve pairings, and only a handful of them matter. Write down the pairs your UI really uses before you write any CSS:
- body text on page background
- heading on page background
- button label on button fill
- link on card background
- border/icon on its surface
Then measure them. The formula is small enough to keep in the project:
const luminance = (hex) => {
const [r, g, b] = hex.replace('#', '').match(/\w\w/g).map((h) => {
const c = parseInt(h, 16) / 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
export const contrast = (a, b) => {
const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m);
return +((x + 0.05) / (y + 0.05)).toFixed(2);
};
contrast('#3C0D4F', '#C6EEA0'); // 11.84 → AAA, ship it
contrast('#F22C33', '#FFFFFF'); // 4.05 → large or bold text only
contrast('#C6EEA0', '#FFFFFF'); // 1.30 → decorative surface, never text
The thresholds that survive code review:
| Ratio | Meaning |
|---|---|
| 4.5:1 | body text — non-negotiable |
| 3:1 | 24px+ text, 19px+ bold, plus borders, icons and focus rings |
| below 3:1 | decoration only: backgrounds, dividers, illustrations, gradients |
Run this on the example palette and it grades itself:
| Swatch | HEX | vs #FFFFFF
|
Verdict |
|---|---|---|---|
#3C0D4F |
15.41:1 | ✅ text, headings, ink | |
#F22C33 |
4.05:1 | 🟡 buttons and big type | |
#C6EEA0 |
1.30:1 | 🚫 surface only | |
#1681DF |
4.01:1 | 🟡 links at 18px+ |
Nothing here is "bad". The table just tells you which color is allowed to be a paragraph and which one is allowed to be a background — a decision you would otherwise make by accident.
🏷️ 3 · Name the colors after their job
This is the step that separates a palette from a theme. Numbered variables are fine for copying and terrible for maintaining:
:root {
- --palette-color-1: #3C0D4F;
- --palette-color-2: #F22C33;
- --palette-color-3: #C6EEA0;
- --palette-color-4: #1681DF;
+ --color-ink: #3C0D4F; /* headings, body text */
+ --color-primary: #F22C33; /* buttons, active states */
+ --color-surface: #C6EEA0; /* cards, highlight sections */
+ --color-link: #1681DF; /* links, info, focus rings */
+ --color-page: #FFFFFF;
}
Now components consume intentions, never hexes:
.card {
background: var(--color-surface);
color: var(--color-ink);
border: 2px solid var(--color-ink);
border-radius: 14px;
padding: 1.5rem;
}
.card__cta {
background: var(--color-primary);
color: #fff;
border-radius: 999px;
padding: 0.7rem 1.4rem;
}
.card a {
color: var(--color-link);
text-underline-offset: 3px;
}
#3C0D4F on #C6EEA0 is 11.84:1, so that card is readable before you even open a checker. And when someone inevitably says "can we try a different palette?", it is one file, not forty components.
🧰 The same four colors as Tailwind config and design tokens
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
ink: '#3C0D4F',
primary: '#F22C33',
surface: '#C6EEA0',
link: '#1681DF',
},
},
},
};
{
"palette": {
"ink": { "$type": "color", "$value": "#3C0D4F" },
"primary": { "$type": "color", "$value": "#F22C33" },
"surface": { "$type": "color", "$value": "#C6EEA0" },
"link": { "$type": "color", "$value": "#1681DF" }
}
}
🧠 4 · Four strategies for actually choosing
Start from the mood. Decide the adjective first — calm, expensive, playful, technical — and search for that word rather than for colors. It is much easier to reject a palette that does not feel "calm" than to compare two blues.
Start from the temperature. Warm (yellow-based) palettes read friendly, human, editorial. Cool (blue-based) palettes read precise, clinical, trustworthy. Getting this wrong is why some fintech dashboards feel like a smoothie brand.
Start from a palette you like and walk sideways. Found something close but the contrast is wrong? Nudge one color's lightness by 15% and re-measure instead of starting over. "Same feeling, better legibility" is nearly always two tweaks away.
Start from what your UI actually needs. Count the roles before you count the colors:
$ what does this UI need?
1 page background
1 card/surface
1 ink (text) ← must clear 4.5:1 on both of the above
1 primary action
1 link colour
3 status colours ← success / warning / danger
= 8 slots, and your palette gave you 4
That is the honest math. A four-color palette is a starting point — you will derive the rest by tinting, shading and mixing:
:root {
--color-primary: #F22C33;
/* derived, not invented */
--color-primary-hover: color-mix(in srgb, var(--color-primary) 85%, black);
--color-primary-subtle: color-mix(in srgb, var(--color-primary) 12%, white);
--color-primary-border: color-mix(in srgb, var(--color-primary) 40%, transparent);
}
🧪 5 · Three quick sanity checks before you commit
Squint test. Blur your eyes at the mockup. If everything mushes into one grey blob, your palette has no contrast hierarchy — a common outcome of very pretty, very muted palettes.
Greyscale test. Screenshot the UI and desaturate it. Buttons should still look like buttons. If two states are identical in greyscale, you are encoding meaning in hue alone, which fails for colorblind users.
One-color-removed test. Delete the accent and look again. If the interface collapses into unusable, you are leaning on decoration to do structural work.
$ checklist
[ ] body text ≥ 4.5:1 on every surface it appears on
[ ] focus ring visible on light AND dark backgrounds
[ ] status colours distinguishable in greyscale
[ ] one text colour that works on the two most common surfaces
[ ] hover/active states derived, not hand-picked
🧾 The short version
- Identify the structure — ramp, base+signals, lights+anchor, or near-monochrome.
- List the pairs your UI ships, and measure only those.
- Rename
--palette-color-3into--color-surface; components never see hexes. - Expect a four-color palette to fill about half your slots; derive the rest with
color-mix(). - Squint, desaturate, and remove the accent before you call it done.
Do that and colour stops being the thing that eats an evening — it becomes four lines in a token file that you can swap in a minute.
If you want a shortcut to step one, ColorFiind is a browsable library of 34,000+ named palettes — every one has its HEX codes, a ready-made :root block and a downloadable swatch image, sorted by category and by season.
Browse 34,000+ colour palettes
What's the palette you're using right now? Drop the four hexes in the comments and I'll tell you which pair is quietly failing. 👇




Top comments (0)