Customization and debugging
Customize with theme variables
Define project design tokens in Tailwind CSS v4 with the theme directive and understand how those tokens create utility classes.
Tailwind v4 is configured in CSS. The @theme directive is where you define your design tokens:
@import "tailwindcss";
@theme {
--color-brand: #2457d6;
--font-display: "Geist", sans-serif;
--breakpoint-3xl: 120rem;
}
Each variable is a design token, a named value the whole project shares. The name prefix tells Tailwind which family of utilities to create:
--color-brandgives youbg-brand,text-brand,border-brand, and every other color utility--font-displaygives youfont-display--breakpoint-3xlgives you the3xl:responsive variant
Save the file, write <button class="bg-brand">, and the button turns blue. No restart, no config file.
They’re real CSS variables too
Tailwind emits theme variables as ordinary custom properties on :root. So you can reference them from plain CSS or from JavaScript:
.chart-line {
stroke: var(--color-brand);
}
Your Tailwind utilities and your hand-written CSS read the same value. Change the token, both update.
@theme or :root?
Use @theme when a value should become part of Tailwind’s utility API. Use :root when you just want a CSS variable at runtime, without generating a family of classes. Both are fine. The difference is whether you want bg-* classes for it.
Which values deserve a token
Tokens come from repeated decisions. A color used once in a marketing illustration doesn’t need a global name. A brand color used for links, buttons, and focus rings does. But test that one value works in every context. A brand blue that passes contrast on white may fail on a gray card.
Raw or semantic names
--color-brand-600 is a raw name, exposing the palette. --color-action is a semantic name, saying what it’s for. Raw scales are flexible. Semantic names make theming easier, because “action” can be blue today and green tomorrow. Many projects use a small semantic layer on top of a restrained palette, and I think that’s the right default.
Breakpoints are a big deal
A custom breakpoint affects every component that uses it. Define it in the same unit as the defaults (rem), keep the list sorted, and write down the content problem it solves. “We got a new phone at the office” is not a reason.
Change tokens carefully
A token change touches everything. Before treating a theme edit as a harmless color swap, check light and dark schemes, hover, focus, and disabled states, and run a visual regression pass if you have one.
Try this: inventory the repeated colors, fonts, and breakpoints in a small page. Promote only three justified values into @theme, replace the repeated classes, and list every state you must review when each token changes.
Lesson completed