Tailwind Palette to Custom Hex — Design Token Handoff
Design delivers #0B6E4F. The codebase is full of green-700. They are not the same. Someone “fixes” it with bg-[#0B6E4F] in a dozen files; two sprints later a dark-mode pass invents the same hex again as a magic string. Tailwind’s default palette is a starting kit, not your brand system. Handoff means mapping hex → named tokens on purpose.
Three outcomes for each designer color
- Exact brand token — Extend the theme (
brand,accent,danger) with the hex and tints if needed. - Nearest Tailwind step — Accept
emerald-700when the delta is invisible in UI chrome and design signs off. - One-off arbitrary — Illustration background used once on a marketing page; still prefer a token if reuse is likely.
Write the decision in a table so Slack arguments die quickly. Nearest-default is a prototype convenience; shipping brand means named tokens.
Example token table (keep this in the repo)
| Token | Hex | Usage | Notes |
|---|---|---|---|
brand.500 | #0B6E4F | Primary buttons | Exact brand |
brand.600 | #095C42 | Hover | Derived |
surface.100 | #F5F7F6 | App background | Near stone-100 |
ink.900 | #14201B | Body text | Exact |
Default palette names (green-700) stay for prototypes; product UI consumes brand.*.
Config sketch (v3-style)
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
500: "#0B6E4F",
600: "#095C42",
},
},
},
},
};
In Tailwind v4, the same idea lands in CSS @theme variables — still one source of truth, still no scattered hex. When you need to explore how a hex sits against Tailwind’s built-in steps, tailwind-color helps compare and translate during the mapping session — then you still record the chosen token name in config, not only the hex.
Handoff workflow that scales
- Collect unique hex from Figma (styles, not random fills).
- Classify: brand / semantic (success-warning-danger) / neutral.
- Generate tints and shades if the design system needs a scale — or import from design tooling.
- Map to Tailwind keys; delete duplicate arbitrary classes in a cleanup PR.
- Document contrast pairs (text on
brand.500must pass AA).
Ask design to name styles brand/500 or color/brand/primary rather than Green final FINAL 2. Export pipelines map cleanly when names already match Tailwind keys. If the file still uses untitled hex swatches, engineering will invent tokens — and invent them twice.
Arbitrary value hygiene
Allowed:
- Last-mile experiment on a branch
- Truly unique illustration stop
Not allowed:
- Same hex in fifteen components
- PR review that cannot name which token a color is
Lint ideas: ban hex in class strings except in the theme file; or codemod #0B6E4F → brand-500. When marketing needs a one-off campaign purple, give it campaign.spring26 with an expiry note in the token table so it does not become permanent primary by accident.
Dark mode and semantic colors
Prefer semantic names (surface, ink, danger) over raw brand in every component. Brand can underpin semantics (--color-primary → brand.500) so dark theme swaps surfaces without rewriting every button. Components should rarely know the hex; they should know the role.
Done criteria for a color handoff
- Theme file contains the brand hex once
- UI classes reference tokens
- README or table lists token ↔ hex ↔ usage
- Contrast checked for text and icon pairs
- Default Tailwind greens and blues not smuggling themselves into primary CTAs
- Dark theme documented as semantic remaps, not a second pile of arbitrary classes
Nearest-default is fine while you explore. Shipping brand means named tokens, documented once, referenced everywhere — with arbitrary hex reserved for genuine exceptions, not daily habit.