# One neutral, one accent

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

Module: Color, depth and surfaces · Level: beginner · 8 min · https://school.agents.org.in/design/surface/palette

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

*[Interactive demo: 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 neutral (The gray ramp that carries most of an interface: backgrounds, borders and text. It can be pure gray or tinted very slightly warm or cool) ramp, 8 to 12 steps of gray. Pure gray is fine; a hint of warm or cool is nicer.
2. One accent (The one saturated brand color, kept for what you can act on: the main button, links, the selected item. It works because almost nothing else is colorful), 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.

> **The purple gradient.** A violet-to-fuchsia gradient, or "a different bright color per card", is the single most recognizable sign of a generated page. Gradient text on a hero headline counts too. Delete it first.

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 ramp (One hue in a row of steps from nearly white to nearly black, usually numbered 50 to 950. A palette is a few ramps, not a pile of colors) 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.

*[Interactive demo: 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.

Primitives (A token that names a raw value by what it is, like --blue-600. Primitives never get used directly inside a component) name a value: `--blue-600`, `--neutral-200`. They say what the color *is* and never change meaning.

Semantics (A token that names a job, like --color-bg-page, and points at a primitive. Components use only these, so a dark theme just repoints them) name a job and point at a primitive: `--color-text-secondary: var(--neutral-700)`. **Components use only these.**

*[Interactive demo: 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 (avoid):**

`--color-blue-button`, `--color-sidebar-gray`, `--color-light-gray` (which is the dark one in dark mode), `--color-text-2`.

  **Named by job (do this):**

`--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 (paste into your agent):**

```text
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.
```

**Check yourself:** A component uses bg-blue-600 directly. You're adding dark mode. What's the fix?

- [ ] Add a dark: variant on that component
- [x] Point the component at a semantic token like --color-accent-solid, and repoint that token for dark
- [ ] Lower the button's opacity in dark mode
- [ ] Invert the colors with a CSS filter

*Why:* Components should reference only semantic tokens. Then dark mode (or any theme) repoints the semantics in one place and no component changes.

## In the skill

- https://school.agents.org.in/skill/surface/color-monochromatic.md
- https://school.agents.org.in/skill/surface/palette-step-roles.md
- https://school.agents.org.in/skill/surface/color-token-tiers.md
