One neutral, one accent
beginner·8 min·Markdown for agents
What this lesson teaches
Build a palette from a gray ramp and one accent, give every step a job, and name your colors so a dark theme is a five-minute change.
Here is the same little project list twice. Flip between them and notice where your eye goes first.
Six colors, all shouting. Nothing to land on.
With a color per thing, everything shouts and nothing wins. In grays with one blue, your eye lands on the blue button and the one live project, which is exactly where it should land.
The whole palette is about fourteen colors
The skill's recipe, borrowed from how Linear, Vercel and Notion work:
- One ramp, 8 to 12 steps of gray. Pure gray is fine; a hint of warm or cool is nicer.
- One , saturated and deliberate, kept for things you can act on.
- Optionally, quiet status colors (danger, warning, success) for the states your product actually shows.
That's it. A tokens file with sixty colors isn't richer; it's undecided.
Three small rules keep the set coherent:
- Neutrals share the brand's temperature. A blue brand wants cool grays. An untinted
#808080next to a tinted surface reads as a placeholder. - Light tints come from lower chroma, not lower opacity. A see-through accent turns gray on some backgrounds.
- Disabled gets its own token, never
opacity: 0.5, which passes contrast on one surface and fails on the next.
Every step has a job
A isn't a gradient to pick from by eye. Each step exists because some part of the UI uses it: the page background, the hover state, the border, the button fill, the text. Tap a role below and watch which parts of the card light up.
Notifications
Choose what reaches your inbox.
White on accent 500: 4.1:1 · on 600: 5.5:1. That is why solid fills sit on 600.
Two naming styles exist. Tailwind numbers steps by lightness (50 is lightest), so in dark mode the mapping flips. Radix numbers them by role (step 9 is always the solid fill, in light and dark), so component CSS never changes. Match the project you're in; for a new system the role model is kinder.
Notice the line under the card. Solid fills sit on 600, not 500, because white text on most 500 hues fails the 4.5:1 contrast you'll meet in the next lesson.
A few more rules that hold across every palette:
- One color, one meaning. Hues within about 15° look like the same color, so a link blue and a decorative blue heading are one meaning used twice.
- Status sits at least 15° from the accent. A red brand can't also use red for "delete"; give destruction a distinct treatment.
- One filled button per view. The fill is what says "primary" from across the room.
Name colors by job, not by look
Now the part that makes a palette survive a redesign. Tokens come in two tiers.
name a value: --blue-600, --neutral-200. They say what the color is and never change meaning.
name a job and point at a primitive: --color-text-secondary: var(--neutral-700). Components use only these.
Semantic → primitive
--color-bg-page--neutral-50--color-bg-surfacewhite--color-text-primary--neutral-900--color-text-secondary--neutral-700--color-border-default--neutral-200--color-accent-solid--blue-600The component reads only semantics
Invite your team
They get access to every project.
Send invites.card { background: var(--color-bg-surface); }
.btn { background: var(--color-accent-solid); }That's why the split matters. If a component says --blue-600 directly, adding dark mode means reading every usage to guess which ones meant "the accent". If it says --color-accent-solid, dark mode repoints one line.
✕ Named by look
--color-blue-button, --color-sidebar-gray, --color-light-gray (which is the dark one in dark mode), --color-text-2.
✓ Named by job
--color-accent-solid, --color-bg-surface, --neutral-200 (a primitive), --color-text-secondary.
Two naming rules from Jakub Krehel's work save real pain:
- One word per concept. Pick
text,bg,border,accentand never mix infg,foregroundorbrand. Someone who has seen--color-text-primaryshould be able to guess--color-text-disabled. - Call the brand
accent, notprimary. Otherwise--color-primary(the brand) sits next to--color-text-primary(body text), the most common collision there is.
Build against a full list of roles from day one (page, surface, raised, sunken; text primary, secondary, disabled, on-accent; border subtle, default, strong, focus; separator), not one token at a time as screens ask. Separator and border are two roles even when they share a color today. They split the first time someone restyles inputs.
Build our color tokens: - One neutral ramp (11 steps, OKLCH, slightly cool to match the blue brand) and one accent ramp. Status colors only for danger and success, each ≥15° of hue from the accent. - Two tiers: primitives named by hue and step (--blue-600), semantics named by role (--color-bg-page, --color-text-secondary, --color-accent-solid). Components use semantics only. - Solid fills on step 600. Disabled gets its own tokens, never opacity. - Before restructuring anything existing, list every color literal in the repo, sorted by hue and lightness, and show me the inventory first.
Quick check
A component uses bg-blue-600 directly. You're adding dark mode. What's the fix?
In the skill
The nodes your agent reads when it does this work.
- Monochromatic palettesMonochromatic with one or two accents beats a five-color palette. Why bright/purple gradients scream AI.
surface/color-monochromatic - Palette step rolesA system is one neutral ramp, one accent ramp and only the status ramps the product renders. Every step exists because a role consumes it (Tailwind 50–950 by lightness, Radix 1–12 by role), solid fills sit on 600 because white on 500 fails, status hues stay 15° from the accent, one filled action per view, and an existing palette is inventoried before it is restructured.
surface/palette-step-roles - Colour token tiersTwo tiers — primitives named by hue and step, never used in a component; semantics named by role, the only tier components reference. Build against a full role inventory, one word per concept, accent for the brand so primary stays free, separator and border as separate roles, and no text on a colour with alpha.
surface/color-token-tiers
Connected lessons
What this lesson builds on and leads to, across every course.