Practical Tailwind Theming with Semantic Design Tokens
Why bg-fun-pink was a mistake, and how renaming Tailwind color tokens by role instead of appearance made this site's design system actually usable.

On this page
For years this site had a Tailwind color called fun-pink. It was cyan. Nobody remembers why. Every time I styled something I had to remember that "pink means cyan", and every shade variant (fun-pink-dark, fun-pink-darker) compounded the lie. This post is about the fix: semantic tokens — naming colors by role, not by appearance.
Appearance names always rot
A color named by appearance makes two promises it can't keep:
- That the value will always look like the name (
fun-pink→#00c7ffbroke this on day one) - That the name tells you where to use it (it never does)
Role names keep both promises:
| Old name (appearance) | New name (role) | Value | Used for |
|---|---|---|---|
fun-pink | accent | #00c7ff | Links, highlights, CTAs |
fun-pink-dark | surface | #192742 | Cards, badges, raised panels |
fun-gray | muted | #7b89a8 | Secondary text, borders |
fun-gray-light | muted-light | #b2bbcf | Body text on dark backgrounds |
dark | bg | #000a1f | Page background |
Now bg-surface text-muted-light reads like a sentence, and changing the accent color is a one-line edit instead of a find-and-replace gamble.
Wiring it up in Tailwind
The tokens live in theme.extend.colors, so Tailwind's defaults stay available:
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
bg: "#000a1f",
accent: "#00c7ff",
"accent-soft": "#009ac5",
surface: "#192742",
"surface-deep": "#000f2e",
muted: "#7b89a8",
"muted-light": "#b2bbcf",
},
},
},
};
export default config;The rule that makes it stick
One rule, enforced in review: no raw hex values in JSX. The moment someone writes text-[#00c7ff], the token system stops being the source of truth. The only places a hex value may appear are the Tailwind config and a single site.ts constant (for the rare JS API that needs a literal color, like a progress bar library).
Migrating an existing codebase
The mechanical part is a scripted rename — fun-pink → accent across the tree. The interesting part is what you find while doing it:
- Off-token colors. A dozen one-off grays that were almost
muted. Each one is a decision: promote it to a token or snap it to an existing one. I snapped almost all of them. - Same role, different values. Two slightly different "card background" colors used interchangeably. That's not a palette, that's an accident. Pick one.
- Dead tokens. Colors referenced nowhere. Delete them; the config should list exactly what the design uses.
Where to stop
Semantic tokens are a sweet spot, not a destination. I deliberately did not add a second theme, CSS variables for runtime switching, or a darkMode config — this site has one dark theme, and speculative flexibility is how design systems become unmaintained. Add the machinery when you have the second theme in hand, not before.


