Your Theme
Tailwind CSS Fundamentals
Chapter 9 · Your Theme: @theme, Variables and Fonts
Every class you have used so far reads from a theme: p-4 from --spacing, bg-blue-500 from --color-blue-500,
rounded-lg from --radius-lg. In Tailwind v3 you changed the theme in a JavaScript file, tailwind.config.js. In v4 you change it in your CSS,
with a directive called @theme. This chapter shows how to add your own colours, fonts, sizes and animations, how to remove or replace the defaults, and the one option
(inline) that decides whether theming by context works.
@theme reference was used for the concepts, and each claim
below was tested rather than assumed. One font file (a Windows system font) was used only for the local test, not published.
What @theme Does
A theme variable is a CSS variable written inside @theme. It does two jobs at once: it becomes a normal CSS variable on the page, and its name
(the part before the first dash after --) tells Tailwind which classes to create. That first part is the namespace:
| Namespace | Creates classes such as | Example variable |
|---|---|---|
| --color-* | bg-mint-500, text-mint-500, border-mint-500, ring-, fill- … | --color-mint-500: oklch(0.72 0.11 178) |
| --font-* | font-display (font family) | --font-display: "Georgia", serif |
| --text-* | text-huge (font size), with --text-huge--line-height | --text-huge: 5rem |
| --spacing, --spacing-* | every padding, margin, gap, width and height class; p-gutter, gap-gutter, w-gutter | --spacing-gutter: 1.5rem |
| --radius-* | rounded-pill | --radius-pill: 9999px |
| --shadow-* | shadow-glow | --shadow-glow: 0 0 20px rgb(34 211 238 / 0.5) |
| --ease-* | ease-snappy | --ease-snappy: cubic-bezier(0.2, 0.9, 0.3, 1) |
| --animate-* | animate-wiggle | --animate-wiggle: wiggle 1s ease-in-out infinite |
| --breakpoint-* | screen variants xs:, 3xl: (chapter 7) | --breakpoint-xs: 30rem |
| --container-*, --font-weight-*, --blur-*, … | @md: and max-w-md; font-bold; blur-sm | the other default namespaces |
I put one variable in each of those namespaces into a theme file, used a class for each, and compiled it. Each class produced a rule that reads its variable (shadows are the one exception, see the note after the list):
A detail: shadow-glow did not read a variable. The generated rule has the shadow written into it
(--tw-shadow: 0 0 20px var(--tw-shadow-color, rgb(34 211 238 / 0.5))), because shadows are built from layers that combine with rings (chapter 3). Use
@theme rather than :root when you want classes: a plain :root { --brand: … } variable is just a variable and creates nothing.
Extending, Overriding and Removing
The three things you can do to the defaults, all measured:
- Extend: use a new name.
--color-mint-500and the others above sit alongside the defaults; every default class still works. - Override: reuse an existing name with a new value. With
@theme { --spacing: 0.5rem; },p-4measured 32px (it is 4 × 0.5rem; it was 16px) andw-64measured 512px. The whole spacing scale doubled, because every size class multiplies that one variable. - Remove: set a whole namespace to
initial. After--color-*: initial; --color-white: #fff; --color-midnight: #121063;I checked the compiled classes:bg-white,bg-midnightandtext-midnightexist, andbg-blue-500andtext-red-500were not generated (an element withtext-red-500kept the plain black text). This is how you replace Tailwind's palette with your brand's, so a straybg-blue-500stops working instead of quietly slipping through.
@theme { --*: initial; } clears every default variable. With --*: initial; --color-midnight: #121063; the class bg-midnight still compiled, but
p-4 and w-64 produced no rules at all, because --spacing was gone too. It is meant for building a design system from nothing; if
you only want your own colours, remove just --color-*.
Theme Variables Are CSS Variables, and Only Used Ones Are Written
Because they are CSS variables, you can use them anywhere: in your own CSS (color: var(--color-mint-500)), in arbitrary values
(rounded-[calc(var(--radius-lg)-1px)]), in inline styles, and in JavaScript with
getComputedStyle(document.documentElement).getPropertyValue('--color-mint-500'). One catch: Tailwind only writes out the theme variables that something uses.
In the theme above, --color-mint-500 was in the compiled file, while --color-unused-500: #123456, which no class used, was not. Usually that is what you
want (a small file), but not when JavaScript or plain CSS needs a variable that no class mentions. Mark such variables with static:
Two more rules, both checked. Theme variables must be at the top level. The documentation says they cannot be nested under a selector or a media query. I put
@theme { --color-nested: #ff0000; } inside @media (min-width: 40rem): Tailwind raised no error, and it emitted --color-nested unconditionally on
:root, so the media condition was silently ignored. If you want a value that changes with screen size, use a normal variable in a normal media query. And
@theme can live in another file: a brand.css containing only the @theme block, pulled in with
@import "./brand.css"; after @import "tailwindcss";, produced bg-brand-500 as expected.
@theme inline: Theming by Context
This is the option that most often goes wrong, so I measured it. Suppose you define the brand colour as an ordinary variable that changes in part of the page, and point a theme variable at it:
With the plain form the theme variable --color-primary is declared once, on :root, and its value is worked out there (the blue --brand) and then
inherited as a finished value, so changing --brand lower down does nothing to it. With inline the utility skips that intermediate variable. I compared the two compiled rules: the plain form generates
.bg-primary { background-color: var(--color-primary); }, the inline form generates .bg-primary { background-color: var(--brand); }, and that
var(--brand) is looked up on each element, so the green island gets green. The rule: use @theme inline
whenever a theme value is a var() of something that changes by context: a data-theme attribute, a dark-mode override, or a font variable supplied by
a framework. It is also what lets you build the multi-brand setups in Tailwind CSS Intermediate/Advanced 2.
Fonts
A font family is a theme variable like any other, in the --font-* namespace. Loading the font is separate: that is plain CSS, written outside
@theme. For a font file you have:
I used a system font file as MyDisplay to test this. With the @font-face present the browser loaded the font and the text came out at a different width than the
default sans; with a name that matches nothing it silently used the generic serif from the fallback list (a third width again). So a misspelled font name does not
fail, it falls back, and the quickest check is the list of loaded fonts (Array.from(document.fonts) with a status of loaded), or the "Rendered Fonts" list at the bottom of DevTools' Computed panel. (document.fonts.check() returned true in both cases, so do not rely on it.)
To change the font of the whole page, override --font-sans: Preflight (chapter 1) sets the page font to --default-font-family, which is
var(--font-sans). With @theme { --font-sans: "Inter", sans-serif; } a paragraph with no classes at all had the computed font family
Inter, sans-serif, while font-mono was unchanged. For a hosted font such as Google Fonts, the stylesheet import has to be the first thing in the
file: @import url("https://fonts.googleapis.com/…"); followed by @import "tailwindcss"; compiled with the font import kept first (I compiled this; I did not
fetch the font). When a framework's font loader gives you a CSS variable, that is the inline case above: @theme inline { --font-sans: var(--font-inter); }.
Your Own Animations
Define the animation as a variable and put its @keyframes inside the same @theme block (as in the first example). Tailwind then writes both the
animate-wiggle class and the keyframes. In Chrome the element had animation-name: wiggle, one running animation (getAnimations().length
was 1), and its transform was a different rotation 350ms later: matrix(0.9988, 0.0486, -0.0486, 0.9988, 0, 0) then matrix(0.9989, -0.0465, 0.0465, 0.9989, 0, 0),
swinging from about +2.8° to −2.7°. Respect the visitor's reduced-motion setting (chapter 8) with motion-safe:animate-wiggle. The other examples in the first table work the same
way: --ease-snappy becomes ease-snappy for transitions, --radius-pill becomes rounded-pill, and a named spacing such as
--spacing-gutter is available to padding, margin, gap and size classes alike.
Your Own Breakpoints
The --breakpoint-* namespace controls the screen variants. I added --breakpoint-xs: 30rem and --breakpoint-3xl: 120rem (next to the five defaults) and
resized the viewport on an element with bg-gray-300 xs:bg-red-500 3xl:bg-green-500:
| Viewport width | Colour | Why |
|---|---|---|
| 479px | gray | below xs (30rem = 480px) |
| 480px | red | xs: applies from 480 |
| 1919px | red | xs: still applies; 3xl (120rem = 1920px) has not started |
| 1920px | green | 3xl: applies and, being larger, overrides xs: |
Tailwind orders the generated media queries by size, not by the order you declared the variables, so a new breakpoint slots into the right place. Changing an existing one is the same:
--breakpoint-sm: 30rem moves sm: (the documentation shows this; I tested adding names, not changing sm).
Hands-On Exercises
Replace Tailwind's palette with a brand palette: keep only white, black and three brand shades (50, 500 and 900), add a font-display family loaded with @font-face and a rounded-pill radius. Build a pill button using them and show from the compiled output and the browser that the brand classes work and that bg-blue-500 and text-red-500 no longer do.
Build a two-theme page: :root blue and [data-theme=green] green, with a bg-primary class. Do it first with a plain @theme and show that the green section stays blue, then fix it with the one-word change and show the green section turns green.
Add a wiggle animation, an xs breakpoint at 30rem, and a --color-primary that JavaScript must be able to read even though no class uses it. Prove the animation is running, show the breakpoint boundary at 479 and 480px, and show the difference between @theme and @theme static for the unused variable.
Chapter 9 Quick Reference
@theme { --namespace-name: value; }makes a CSS variable and the classes for its namespace:--color-*,--font-*,--text-*(+--text-x--line-height),--spacing/--spacing-*,--radius-*,--shadow-*,--ease-*,--animate-*,--breakpoint-*,--container-*. Use:rootfor variables that should NOT create classes- Extend with a new name; override with an existing name (
--spacing: 0.5rem:p-4= 32px); remove a namespace with--color-*: initial(bg-blue-500stops existing);--*: initialremoves everything, including spacing - Only used theme variables are written to the output;
@theme staticforces them (JavaScript can then read them);@thememust be top level (inside@mediathe condition is silently ignored); it can live in an imported file @theme inlinewhen a theme value isvar(--something)that changes by context (data-theme, dark overrides, framework font variables); plain@themeresolves it once on:root- Fonts:
@font-faceoutside@theme, then--font-display: "Name", serif; a wrong name silently falls back; override--font-sansto change the page font;@import url()before@import "tailwindcss" - Animations:
--animate-wiggle: wiggle 1s infiniteplus@keyframes wiggle {…}inside@theme; addmotion-safe: - Breakpoints:
--breakpoint-xs: 30remgivesxs:(from 480px); generated in size order
@utility and @variant, @layer, @apply and
when to write ordinary CSS instead.