Skip to main content
ASoc
Tutorial

Tailwind Colors: What @theme Actually Overrides, and the Scale We Never Finished

Redeclaring a default Tailwind color name overrides it everywhere; a new name just adds one. This codebase's own token file shows both, plus a scale that was never built for error states.

The ASoc Team7 min read

Tailwind v4 defines colors as CSS custom properties inside an @theme block in your stylesheet, not a JavaScript config object — and redeclaring a name Tailwind already ships (gray, red, blue) overrides that palette everywhere, while a new name (primary) just adds one. This storefront's own token file shows what happens when that distinction isn't tracked consistently: two fully-built 12-step scales, one 5-step scale, and — found by grepping the components that actually use color — zero tokens at all for error states, so fifteen call sites reach for seven different shades of Tailwind's stock red.

Where the colors actually live

Everything is one file, src/app/globals.css, inside a single @theme block:

@theme {
  --color-primary: #465fff;
  --color-primary-25: #f2f7ff;
  /* … 25 through 950, 12 stops … */

  --color-gray-25: #fcfcfd;
  /* … 25 through 950, 12 stops … */

  --color-text-color: #344054;
  --color-title-color: #1d2939;
  --color-stroke: #e4e7ec;

  --color-success-50: #ecfdf3;
  --color-success-500: #12b76a;
  --color-success-700: #027a48;
}

Every --color-* variable in that namespace becomes a utility class automatically — --color-primary-600 is bg-primary-600, text-primary-600, border-primary-600, no separate config step. That's the whole of Tailwind v4's "CSS-first" pitch: the token is the utility generator, so there's no build step translating a JS object into classes.

Reusing a name overrides it; inventing one adds it

This file does both, in the same block, and they behave differently. primary isn't a name Tailwind ships by default, so those twelve declarations are pure addition — nothing existed at that name before. gray is a name Tailwind ships by default (a full OKLCH-based 11-step scale), so redeclaring all twelve of its steps replaces the built-in palette at that name for the entire app. Every bg-gray-500 anywhere in this codebase — and there are hundreds — resolves to this file's #667085, not Tailwind's stock gray. Nothing marks that override as intentional in the file itself; you only find it by knowing gray is a reserved name and checking whether every step got redeclared. It has: this codebase overrides the complete default gray scale, not a spot value, so there's no fallback shade sneaking in from Tailwind's built-in palette anywhere in the app.

Scale tokens and semantic tokens are not the same thing, and this file doesn't wire them together

--color-gray-700 is a scale token — a rung on a numbered ladder. --color-text-color is a semantic token — a name that describes a use, not a position. This file defines both, and the semantic ones are meant to track specific rungs:

Semantic tokenValueMatches
--color-text-color#344054gray-700
--color-text-color-secondary#667085gray-500
--color-text-color-tertiary#98a2b3gray-400
--color-title-color#1d2939gray-800

They match today because someone typed the same hex twice — --color-text-color is a literal duplicate of #344054, not var(--color-gray-700). Nothing in CSS enforces that the two stay equal. Change --color-gray-700 to retune the neutral scale and every bg-gray-700 in the app updates instantly; --color-text-color does not move, because it was never wired to that variable — it's an independent hex string that happened to start out matching. This is the one thing in the file worth fixing, and the fix is mechanical: --color-text-color: var(--color-gray-700); costs nothing and removes the drift risk entirely.

The scale that never got finished

success is a real token family — five stops (50, 100, 500, 600, 700), enough to cover a badge background, a badge text color, and a hover state. There is no matching error or warning family anywhere in @theme. Grepping the component tree for how error states are actually styled shows what fills that gap:

src/components/organisms/AccountSettingsSection.tsx:  text-red-700 dark:text-red-400
src/components/molecules/RefundButton.tsx:             text-red-600 dark:text-red-400
src/components/molecules/PurchaseRow.tsx:               bg-red-50 … text-red-600 dark:bg-red-500/10
src/components/molecules/SettingsForm.tsx:              border-red-300 bg-red-50/60 … text-red-700
src/components/molecules/AuthCard.tsx:                  text-red-500 dark:text-red-400

Fifteen call sites across the codebase, and seven distinct red shades among them (50, 200, 300, 400, 500, 600, 700) with no single one used consistently for "this is an error." Each component author picked a shade that looked right in isolation. Compare that to every success-600 reference in the same files — one token, one shade, everywhere. The fix isn't more red judgment calls; it's a --color-error-* family sized the same as success (a background tint, a mid text shade, a hover), so the next component reaches for a token instead of guessing a number.

The edit that silently does nothing

This project's own CLAUDE.md carries a warning specific to this file, in capital letters, because the failure mode is silent: Tailwind v4 + Turbopack does not hot-reload @theme changes. Add --color-brand-500 to the block while npm run dev is running, save, and bg-brand-500 in your markup generates no CSS — not an error, just nothing, because Turbopack's dev pipeline doesn't currently pick up new theme variables without a fresh process. The fix is a full restart of the dev server, not a hot-reload wait or a hard browser refresh, and it costs real time only because the failure looks identical to a typo: no console warning distinguishes "you misspelled the class" from "you're right, but the server hasn't seen your token yet."

Mistakes and how they show up

MistakeSymptomFix
Redeclaring a default color name without meaning toEvery use of that name site-wide silently changes shadeCheck whether the name is a Tailwind default before choosing it; if overriding on purpose, comment why
Semantic tokens as literal hex instead of var() aliasesEditing the base scale doesn't move the semantic tokens that were "supposed to" track it--color-text-color: var(--color-gray-700);, not a copied hex string
No token for a whole state (error, warning)Every component picks its own shade; no visual consistency, no single place to retune itSize a scale for the state the same way success is sized — a tint, a mid shade, a hover
Editing @theme while next dev (Turbopack) is runningNew utility classes generate nothing, with no errorRestart the dev server after any @theme edit, every time
Assuming --color-primary and --color-primary-500 are independentThey're deliberately the same value here — the bare name is an alias for the default stepKeep a scale's bare name equal to whichever step is meant to be "the" color, or drop the bare alias

Frequently asked questions

Do I need initial to fully override a default Tailwind color? Only if you're not redeclaring every step. --color-gray-*: initial; clears the whole default family in one line, useful for a partial replacement. Redeclaring all twelve steps yourself, as this file does, overrides the family completely without it — there's no default step left unshadowed to clear.

Should I extend Tailwind's default palette or replace it with my own scale? This file does neither cleanly — it replaces gray entirely (by reusing the name) while leaving every other Tailwind default (red, blue, amber, …) untouched and available. That's a reasonable split for a brand with one dominant neutral and one accent, but it means "is this a default Tailwind color or ours?" isn't answerable by reading a class name alone — you have to check the token file.

What's the actual difference between a scale token and a semantic token? A scale token (gray-700) is positional — its meaning is "darker than 600, lighter than 800." A semantic token (text-color) is descriptive — its meaning is "what body text uses," independent of which rung that happens to be today. Semantic tokens should reference scale tokens with var(), not duplicate their values, precisely so a scale retune propagates.

Why didn't my new color class show up after I edited globals.css? Almost certainly the Turbopack @theme hot-reload gap above — restart npm run dev before assuming the class name is wrong.

Templates with color systems worth studying

ASoc Echo markets an AI support product with a live chat-widget mockup and a conversation-analytics panel — status colors (resolved, pending, escalated) are exactly the kind of state a missing token family makes inconsistent. ASoc Edge is built around a unified "AI control room" dashboard section, where a coherent accent scale across charts and status badges matters more than in a typical marketing page. ASoc Fade takes the opposite approach — a single warm accent across a services grid, price list and booking flow, the case for a small, deliberate palette over a large generated one.

Browse the full set of Next.js landing page templates or Tailwind landing page templates.

Keep reading

Tutorial9 min read

Tailwind Container Queries: One @container, Zero Queries

Our own home page had the container context and none of the queries — so a card loses a third of its width at lg while its padding never moves. The arithmetic and the fix.

Read more
Tutorial11 min read

Tailwind v4 Dark Mode: We Wrote 480 Variants and Shipped No Toggle

One @custom-variant line replaces darkMode: 'class'. Then you choose: dark: at every call site, or tokens under .dark. We picked the first 480 times — and found nothing can turn it on.

Read more
Tutorial9 min read

A Tailwind Data Table Audit: One Real Table, Two Real Defects

The only <table> in this codebase, read cell by cell: what it gets right, the missing caption and text-free checkmarks it shipped with, and the fix.

Read more