tailwind-token-consolidation
Consolidate, dedupe, or reduce CSS custom properties and Tailwind v4 design tokens in globals.css while preserving the same visual result.
Install
npx skills add https://github.com/millionco/skills --skill tailwind-token-consolidationTailwind Token Consolidation
You are reducing the count of design tokens in a Tailwind v4 globals.css while preserving visual fidelity.
REQUIRED, in order, for every consolidation task:
- Audit first, edit never-first. Parse
globals.cssinto structured blocks (@theme inline,:root,.dark, every other scoped block). Count tokens. Build a usage map by scanning ALL.ts/.tsx/.css/.mdxfiles (excludingglobals.css) for class names ANDvar(--...)refs. - Compute the LIVE set transitively. A token is live if (a) its suffix appears as a Tailwind class anywhere, OR (b) it is referenced as
var(--name)anywhere, OR (c) it is referenced inside another live token's value. Iterate to a fixed point. - Scan
globals.css's own utility classes too. Anyvar(--...)inside.X { ... }blocks (outside:root/.dark/@theme inline) is a hard dependency. Tokens demanded by those classes MUST stay. - Build a deterministic rename map as data (
old_suffix -> new_suffixorDELETE). Apply it to globals.css AND code in a single script. Never hand-edit class names across the codebase. - Verify after every pass: run
pnpm typecheck,pnpm lint,pnpm format,pnpm build. All must pass before declaring done.
Do not skip any step. A skipped step is a failed task.
The Two Reduction Levers (apply in this order)
DO NOT inline tokens into Tailwind arbitrary values (px-[17px], text-[16px]/[1.6875rem], shadow-[#000_0px_1px]). Inlining trades named-token clutter for arbitrary-value clutter — same noise, different file. Named tokens carry intent; arbitrary values destroy it. Realistic reduction with these two levers alone is 50–60%, not 70%. If the user asks for a higher number, push back: the right answer is fewer tokens, not the same noise in className strings.
The Eight Bugs You Will Hit (anticipate them)
- Value precedence bug. When N tokens collapse to one canonical name, first-write-wins from parse order is unpredictable. The result is often the WRONG color (e.g.,
--dangerends up holding--diagnostics-table-error-bordervalue, which is the pale border tint, not the bright red). Fix: add an explicitCANONICAL_SOURCEoverride dict mapping new tokens to a specific source token whose value should win. - Scoped override bug.
.dashboard-page { --text-inline: 14px; }only works because--text-inlineis avar()-referenced custom property. Tokens overridden in any scoped block (.dashboard-page,.dark .dashboard-page, etc.) MUST stay as tokens; deleting them silently breaks the scoped override. - Intra-globals consumer bug. Utility classes inside
globals.css(e.g.,.dropdown-menu-highlight { padding: var(--spacing-dropdown-menu-padding); }) read tokens that don't appear in any.tsxfile. Deleting these from@themeproduces a runtime-undefined CSS var. Fix: strip@theme inline/:root/.dark/etc. blocks from the CSS text before scanning, then collect everyvar(--X)ref in the remainder. Those tokens MUST stay. - Category cross-pollination bug. A suffix can appear in multiple categories. For example,
organization-section-surfaceis a COLOR (white) AND a SHADOW alias prefix (--shadow-organization-section-surface). If your map has both'organization-section-surface': 'card'(color) and'organization-section-surface': 'surface-soft'(shadow), the second one will overwrite the bare-token rename in your global map. Fix: only the COLOR category renames the bare--Xtoken. For SHADOW/SPACING/TEXT/RADIUS/FONT/TRACKING, rename ONLY the--<theme-prefix>-Xalias, never the bare name. - Aggressive icon-color collapse bug. Collapsing every "grey icon" (#696969, #898989, #b3b3b3, #c1c1c1, #dddddd) to a single
--icontoken makes the originally-light icons look noticeably darker — and on dark surfaces (e.g., dropdown menus that stay dark in both modes), it inverts the intended contrast. Fix: keep TWO icon tones:--icon(#696969 mid-grey) and#b3b3b3 light-grey). Map originally-light icons to--icon-soft(--icon-soft. - Surface-with-conditional-transparency bug. Some tokens are
transparentin one mode and a concrete color in the other (e.g.,--repository-card-panel: #ffffffin light,transparentin dark — the dark version is intentionally see-through). Collapsing into--cardmakes the panel opaque in dark mode, visibly changing the layered look. Fix: if a token's value istransparentin one mode and concrete in the other, do NOT collapse it with an always-opaque token. Either keep it separate or inline the conditional logic in the consuming className. bg-Xcolor/image collision bug. Tailwind v4 resolvesbg-Xagainst BOTH--color-X(background-color) and--background-image-X(background-image). If only--background-image-Xexists,bg-Xpaints the gradient correctly. But if you rename some OTHER token (e.g.,--install-button-text) to--install-button, you create a--color-install-buttonalias — and nowbg-install-buttonmay setbackground-color: whiteon top of the gradient, making the button look empty/invisible. Fix: before collapsing a color suffix to nameX, grep for--background-image-Xand anybg-Xclass usage. If either exists, pick a different target name (e.g., keep--install-button-textinstead of renaming to--install-button).- Always-dark surface bug. Tokens for dropdown menus, tooltips, popovers-that-stay-dark, etc., have text/icon colors that are LIGHT in BOTH light and dark modes (because the surface stays dark regardless of theme). Collapsing
--dropdown-menu-textinto--foregroundmakes the dropdown text dark in light mode (dark text on dark surface = invisible). Fix: before collapsing a token, diff its light-mode and dark-mode values. If both values are on the same side of the L=0.5 line (both light or both dark), it lives on an always-light or always-dark surface and CANNOT be collapsed with a flipping token like--foregroundor--icon. Keep it as its own token.
The Three-Pass Block Transformation
When emitting a :root or .dark block, write entries in THIS order:
This guarantees: (a) the canonical token's original value always wins over a rename, (b) you explicitly control which source provides the value for NEW tokens like --danger, and (c) duplicate entries never appear in the output.
Apply-And-Verify Loop
Run this loop after EVERY rename-map change:
If any check fails, fix the map (not the generated output), then re-run. Hand-editing the generated globals.css is a footgun: the next transformation run will overwrite your fix.
Rationalization Table
Before Declaring Done
You MUST have:
- Run
pnpm typecheck— exit 0. - Run
pnpm lint— 0 errors, 0 warnings. - Run
pnpm format— completed. - Run
pnpm --filter web build— exit 0. - Counted final tokens with
rg "^\s*--[a-z][a-zA-Z0-9-]*\s*:" apps/web/app/globals.css -o | sort -u | wc -l. Report before/after counts and the percentage reduction. - Confirmed at least the top 3 visible surfaces (cards, page background, dropdowns) render with the intended colors by reading the resulting
:rootblock end-to-end and verifying canonical values match what you expected.
A green build with broken visual output is a failed task. A counted reduction without verification is a failed task.