Skip to content
ExamsLibraryFor agentsConnect your agent

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.

  1. 1The whole palette is about fourteen colors
  2. 2Every step has a job
  3. 3Name colors by job, not by look

Here is the same little project list twice. Flip between them and notice where your eye goes first.

ProjectsNew project
Website refreshDesignFri
Billing migrationBackendMon
Onboarding emailsCopyTue

Six colors, all shouting. Nothing to land on.

Six colors compete; one accent leads.

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:

  1. One ramp, 8 to 12 steps of gray. Pure gray is fine; a hint of warm or cool is nicer.
  2. One , saturated and deliberate, kept for things you can act on.
  3. 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 #808080 next 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.

Weekly digest
Every mention
you@studio.com
Save

White on accent 500: 4.1:1 · on 600: 5.5:1. That is why solid fills sit on 600.

Tap a role. Toggle the names to see Tailwind's numbers and Radix's.

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-600

The 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); }
Switch to dark. The primitives and the component stay as written; only the arrows move.

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, accent and never mix in fg, foreground or brand. Someone who has seen --color-text-primary should be able to guess --color-text-disabled.
  • Call the brand accent, not primary. 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.

Set up a palette· for your agent
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?

What this lesson builds on and leads to, across every course.

  • Routing the job
  • Color you can do math with
  • Contrast you can read
  • Designing a dark theme