Customising with @theme

Tailwind CSS · Lesson 20 of 27 7 min read Updated

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:

CSS
@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">:

HTMLTry it yourself
<!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:

NamespaceExample variableExample utilities
--color-*--color-saffron-500bg-saffron-500, text-*, border-*, fill-*
--font-*--font-displayfont-display
--text-*--text-herotext-hero (font size)
--font-weight-*--font-weight-heavyfont-heavy
--tracking-*, --leading-*--leading-snugtracking-*, leading-*
--breakpoint-*--breakpoint-3xl3xl: variant
--container-*--container-8xl@8xl: variant, max-w-8xl
--spacing--spacing: 0.25remp-4, m-2, gap-6, w-64, every spacing number
--radius-*--radius-cardrounded-card
--shadow-*--shadow-softshadow-soft
--ease-*, --animate-*--ease-snappyease-snappy, animate-*
--aspect-*, --blur-*--aspect-posteraspect-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:

CSS
@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:

HTMLTry it yourself
<!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:

CSS
.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:

CSS
/* brand.css */
@theme {
  --color-brand: oklch(58% 0.16 242);
  --font-display: "Poppins", sans-serif;
}
CSS
/* app.css in each project */
@import "tailwindcss";
@import "./brand.css";

Common mistakes

  • Wrong namespace. --colour-brand or --colors-brand creates nothing. Use the exact namespace, such as --color-brand.
  • Defining theme variables in :root. A variable in :root is just a CSS variable. Only variables inside @theme create utilities.
  • Expecting the font to download. --font-display only names the font; load the font file separately.
  • Removing all colours by accident. --color-*: initial removes white, black and transparent too, unless you redefine the ones you need (transparent and current are 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-*: initial to remove a whole namespace.
  • --spacing controls the entire spacing scale.
  • Theme variables are real CSS variables: use var(--color-brand) in CSS, inline styles and JavaScript.
  • Use @theme inline for variables that reference other variables, and @theme static to 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
HTML
<!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?

2. What does --color-*: initial; inside @theme do?

3. Your theme sets --spacing: 0.3rem. What is the padding of p-10?

4. Where must a variable be declared for Tailwind to create utilities from it?

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