Customising with @theme
Every project has its own brand: a particular blue, a heading font, a preferred corner radius. Tailwind's default theme is a good start, but you will want to add your own design tokens and sometimes remove the ones you never use. In Tailwind 4 you do all of this in CSS with the @theme directive. There is no JavaScript configuration file to learn.
In this lesson you will add colours, fonts, sizes and shadows, see which variable names create which utilities, override and reset the defaults, and use the same values in your own CSS and JavaScript.
Your first theme variables
A theme variable is a CSS custom property declared inside @theme. Tailwind reads it and creates matching utilities:
@import "tailwindcss";
@theme {
--color-saffron-500: oklch(72% 0.17 55);
--color-saffron-700: oklch(55% 0.15 50);
--font-display: "Poppins", sans-serif;
--radius-card: 1.25rem;
}Now bg-saffron-500, text-saffron-700, border-saffron-500/40, font-display and rounded-card all exist, and they work with every variant: hover:bg-saffron-700, md:rounded-card, dark:text-saffron-500. These are added to the default theme; all the built-in colours are still there.
In the browser build, put @theme inside <style type="text/tailwindcss">:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brand theme</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--color-saffron-50: oklch(97% 0.03 75);
--color-saffron-500: oklch(72% 0.17 55);
--color-saffron-700: oklch(55% 0.15 50);
--font-display: Georgia, "Times New Roman", serif;
--radius-card: 1.25rem;
--shadow-soft: 0 10px 30px rgb(0 0 0 / 0.08);
}
</style>
</head>
<body class="bg-saffron-50 p-8">
<article class="max-w-sm rounded-card bg-white p-6 shadow-soft">
<h1 class="font-display text-3xl font-bold text-saffron-700">Annapurna Tiffin</h1>
<p class="mt-2 text-slate-600">Home-style thalis delivered across Pune, ₹120 a meal.</p>
<a href="#" class="mt-4 inline-block rounded-lg bg-saffron-500 px-4 py-2 font-semibold text-white hover:bg-saffron-700">Order today</a>
</article>
</body>
</html>To use a web font, load it as usual (for example a Google Fonts <link> in the page <head>, or @font-face in your CSS) and then name it in --font-*. Theme variables only give fonts a class name; they do not download them.
Namespaces: which variable makes which utility
The part of the name before the key is the namespace. It decides which utilities the variable powers:
| Namespace | Example variable | Example utilities |
|---|---|---|
--color-* | --color-saffron-500 | bg-saffron-500, text-*, border-*, fill-* |
--font-* | --font-display | font-display |
--text-* | --text-hero | text-hero (font size) |
--font-weight-* | --font-weight-heavy | font-heavy |
--tracking-*, --leading-* | --leading-snug | tracking-*, leading-* |
--breakpoint-* | --breakpoint-3xl | 3xl: variant |
--container-* | --container-8xl | @8xl: variant, max-w-8xl |
--spacing | --spacing: 0.25rem | p-4, m-2, gap-6, w-64, every spacing number |
--radius-* | --radius-card | rounded-card |
--shadow-* | --shadow-soft | shadow-soft |
--ease-*, --animate-* | --ease-snappy | ease-snappy, animate-* |
--aspect-*, --blur-* | --aspect-poster | aspect-poster, blur-* |
A font size can carry its own line height with a double-dash suffix: --text-hero: 4rem; --text-hero--line-height: 1.1; makes text-hero set both.
Overriding and removing defaults
Redefining an existing variable overrides it. Setting a namespace to initial removes all its defaults:
@theme {
--breakpoint-sm: 36rem; /* change an existing value */
--color-*: initial; /* remove every default colour... */
--color-white: #fff; /* ...then define only your palette */
--color-ink: oklch(25% 0.03 260);
--color-brand: oklch(58% 0.16 242);
}After this, bg-red-500 no longer exists, and IntelliSense only suggests your colours. That is a good way to keep a team on-brand. --*: initial removes the entire default theme, for projects that want to define every token themselves.
Changing --spacing changes the whole spacing scale at once, because every spacing utility multiplies it. Try it in the next example, which also sets one custom spacing key, --spacing-header, that creates h-header, pt-header and similar:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spacing scale</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--spacing: 0.3rem; /* default is 0.25rem; try 0.2rem or 0.4rem */
--spacing-header: 4rem;
}
</style>
</head>
<body class="pt-header">
<header class="fixed inset-x-0 top-0 flex h-header items-center bg-slate-900 px-6 text-white">Rohtak Cycles</header>
<div class="flex flex-wrap gap-4 p-6">
<div class="rounded-lg bg-sky-100 p-4">p-4</div>
<div class="rounded-lg bg-sky-100 p-8">p-8</div>
<div class="rounded-lg bg-sky-100 p-12">p-12</div>
</div>
</body>
</html>Change --spacing to 0.4rem and run it again: every padding and gap grows together, so the design stays in proportion.
Theme variables are real CSS variables
Theme variables end up in your stylesheet as normal custom properties on :root, so you can use them anywhere CSS works:
.price-tag {
background: var(--color-saffron-50);
border-radius: var(--radius-card);
padding: calc(var(--spacing) * 3);
}They also work in inline styles (style="color: var(--color-brand)"), in arbitrary values (bg-(--color-brand)/20), and in JavaScript with getComputedStyle(document.documentElement).getPropertyValue('--color-brand'). To keep files small, Tailwind only outputs variables that something uses. If you need all of them available, for example for JavaScript, write @theme static { ... } and they are always emitted.
Sharing a theme between projects
Because the theme is just CSS, you can keep it in its own file and import it wherever you need it, for example a company's main site and its admin panel:
/* brand.css */
@theme {
--color-brand: oklch(58% 0.16 242);
--font-display: "Poppins", sans-serif;
}/* app.css in each project */
@import "tailwindcss";
@import "./brand.css";Common mistakes
- Wrong namespace.
--colour-brandor--colors-brandcreates nothing. Use the exact namespace, such as--color-brand. - Defining theme variables in
:root. A variable in:rootis just a CSS variable. Only variables inside@themecreate utilities. - Expecting the font to download.
--font-displayonly names the font; load the font file separately. - Removing all colours by accident.
--color-*: initialremoveswhite,blackandtransparenttoo, unless you redefine the ones you need (transparentandcurrentare keywords and keep working).
Key points
- Add design tokens with
@theme { ... }in your CSS; each variable creates utilities. - The namespace decides the utilities:
--color-*,--font-*,--text-*,--radius-*,--shadow-*,--breakpoint-*and more. - Redefine a variable to override it; set
--color-*: initialto remove a whole namespace. --spacingcontrols the entire spacing scale.- Theme variables are real CSS variables: use
var(--color-brand)in CSS, inline styles and JavaScript. - Use
@theme inlinefor variables that reference other variables, and@theme staticto always output them.
Exercise
Create a theme for "Pink City Crafts", a handicraft shop in Jaipur. Add a pink-city colour with shades 100, 500 and 800, a --font-display serif font stack, a --radius-tile of 0.875rem, and a custom breakpoint xs at 30rem. Use them on a product tile whose title uses the display font and whose layout changes from a column to a row at the xs breakpoint.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pink City Crafts</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--color-pink-city-100: oklch(94% 0.04 10);
--color-pink-city-500: oklch(66% 0.16 10);
--color-pink-city-800: oklch(40% 0.12 10);
--font-display: Georgia, "Times New Roman", serif;
--radius-tile: 0.875rem;
--breakpoint-xs: 30rem;
}
</style>
</head>
<body class="bg-pink-city-100 p-6">
<article class="flex max-w-md flex-col gap-4 rounded-tile bg-white p-4 xs:flex-row xs:items-center">
<img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" alt="Blue pottery vase" class="aspect-video w-full rounded-tile object-cover xs:w-36">
<div>
<h2 class="font-display text-xl font-bold text-pink-city-800">Blue pottery vase</h2>
<p class="text-pink-city-500">₹1,850</p>
</div>
</article>
</body>
</html>Every variable creates utilities from its namespace: the colours give bg-pink-city-100 and text-pink-city-800, --radius-tile gives rounded-tile, and --breakpoint-xs creates the xs: variant.
Quick Quiz
1. Which theme variable creates a bg-mango utility?
Colour utilities come from the --color-* namespace, so --color-mango creates bg-mango, text-mango and others.
2. What does --color-*: initial; inside @theme do?
Setting a namespace to initial removes its default values.
3. Your theme sets --spacing: 0.3rem. What is the padding of p-10?
Spacing utilities multiply --spacing by the number: 10 × 0.3rem = 3rem.
4. Where must a variable be declared for Tailwind to create utilities from it?
Only variables declared in @theme become theme variables that generate utilities.
Learn Tailwind CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
