Tailwind CSS Handbook v4.3

Functions & Directives

A reference for every Tailwind v4 CSS directive and function — @import, @theme, @source, @utility, @variant, @config, --alpha() and --spacing().

Tailwind v4 is configured in CSS, which means the API surface is a small set of at-rules and functions your stylesheet can use. This page documents every one of them: what it does, its exact syntax, and one realistic example.

Overview#

Directive / functionPurpose
@importPull in Tailwind, or any other stylesheet
@themeDefine design tokens and generate utilities
@sourceRegister extra files for class detection
@source notExclude files from class detection
@source inline()Safelist classes that never appear in source
@utilityDefine a custom utility
@variantApply a Tailwind variant inside your own CSS
@custom-variantDefine a custom variant
@applyInline existing utilities into a rule
@referenceMake definitions visible to a scoped stylesheet
@configLoad a legacy tailwind.config.js
@pluginLoad a JavaScript plugin
@layerPlace rules in a cascade layer
--alpha()Adjust a colour's opacity
--spacing()Compute a value from the spacing scale
--theme()Read a theme value where var() is not allowed
theme()Deprecated Dot-notation theme access

@import#

Inlines another stylesheet, including Tailwind itself. This is the entry point of every v4 project.

Syntax
@import "tailwindcss";
@import "./typography.css";
app.css
@import "tailwindcss";
@import "./components/buttons.css";

@theme {
  --color-brand-500: oklch(0.62 0.19 264);
}

Import options#

@import "tailwindcss" accepts two modifiers that control source detection:

CSS
/* Set the base directory for automatic detection */
@import "tailwindcss" source("../src");

/* Turn automatic detection off entirely */
@import "tailwindcss" source(none);

source(none) is worth knowing about for monorepos and libraries where you want to enumerate every scanned path explicitly.

@theme#

Defines design tokens. Every variable in a @theme block becomes both a CSS custom property and a set of utilities, according to its namespace.

Syntax
@theme { --namespace-name: value; }
@theme inline { … }
@theme static { … }
CSS
@theme {
  --font-display: "Satoshi", sans-serif;
  --breakpoint-3xl: 120rem;
  --color-avocado-500: oklch(0.84 0.18 117.33);
  --ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}
OptionEffect
(none)Utilities reference the variable — background-color: var(--color-avocado-500)
inlineUtilities inline the variable's value, so runtime changes to a referenced variable take effect
staticEmit every variable, even ones no utility uses — for reading tokens from JavaScript
CSS
/* A token whose value is set at runtime by next/font */
@theme inline {
  --font-sans: var(--font-inter);
}

/* Tokens a charting library reads via getComputedStyle */
@theme static {
  --color-chart-1: oklch(0.7 0.15 200);
  --color-chart-2: oklch(0.7 0.15 30);
}

Full coverage, including namespaces and how to remove defaults, is in Theme & Design Tokens.

@source#

Registers a path for class detection. Tailwind already scans your project automatically — skipping .gitignored paths, node_modules, binaries, CSS files and lockfiles — so @source is for the places that sweep misses.

Syntax
@source "path";
CSS
@import "tailwindcss";

/* A design-system package that ships class names in its dist files */
@source "../node_modules/@acmecorp/ui-lib";

Paths are resolved relative to the stylesheet the directive appears in.

@source not#

v4.1 Excludes a path from detection. Useful for large directories you know contain no Tailwind classes, or that contain text that looks like classes.

Syntax
@source not "path";
CSS
@import "tailwindcss";

@source not "../src/components/legacy";
@source not "../public/vendor";

The payoff is build time, and occasionally correctness — a directory of scraped HTML or fixture data can otherwise inflate your output with utilities nothing renders.

@source inline()#

v4.1 Generates classes as if they had been found in a source file. This is v4's replacement for the v3 safelist option.

Syntax
@source inline("classes");
@source not inline("classes");
CSS
@import "tailwindcss";

@source inline("underline");

Brace expansion generates families of classes without listing each one:

CSS
/* The class, plus its hover and focus variants */
@source inline("{hover:,focus:,}underline");

/* Every red shade, plus hover variants: bg-red-50 … bg-red-950 */
@source inline("{hover:,}bg-red-{50,{100..900..100},950}");

The {start..end..step} form is a numeric range. {a,b,} is a literal list, and a trailing empty item means "and also the bare class".

@source not inline(…) does the opposite — it prevents classes from being generated even when they do appear in your source:

CSS
@source not inline("{hover:,focus:,}bg-red-{50,{100..900..100},950}");

@utility#

Defines a custom utility that works with every variant and sorts correctly against the built-ins.

Syntax
@utility name { … }
@utility name-* { … }
CSS
@utility content-auto {
  content-visibility: auto;
}

@utility tab-* {
  tab-size: --value(--tab-size-*, integer, [integer]);
}

Functional utilities read their argument with --value(), their slash modifier with --modifier(), and their no-argument fallback with --default(). See Custom Utilities for the full treatment.

@variant#

Applies an existing Tailwind variant to styles you write yourself, so you never have to hand-copy the selector or media query a variant compiles to.

Syntax
@variant name { … }
@variant a:b { … }
@variant a, b { … }
CSS
.my-element {
  background: white;

  @variant dark {
    background: black;
  }
}

v4.3 Variants may be stacked with colons or combined with commas:

CSS
.button {
  background: var(--color-sky-500);

  /* Hovered AND focused */
  @variant hover:focus {
    background: var(--color-sky-600);
  }
}

.link {
  color: var(--color-sky-500);

  /* Hovered OR focused */
  @variant hover, focus {
    color: var(--color-sky-600);
  }
}

@custom-variant#

Registers a new variant name.

Syntax
@custom-variant name (selector);
@custom-variant name { … @slot; … }
CSS
/* Shorthand */
@custom-variant theme-midnight (&:where([data-theme="midnight"] *));

/* Block form, for multiple nested rules */
@custom-variant any-hover {
  @media (any-hover: hover) {
    &:hover {
      @slot;
    }
  }
}

@slot marks where the utility's declarations are inserted. See Custom Variants.

@apply#

Inlines existing utility classes into a rule.

Syntax
.selector { @apply utility utility; }
CSS
/* Styling a third-party widget's markup */
.select2-dropdown {
  @apply rounded-b-lg shadow-md;
}

.select2-search {
  @apply rounded border border-gray-300;
}

@apply is for markup you cannot edit. It is not a way to build a component layer — see When @apply is the right tool.

@reference#

Makes your theme variables, custom utilities and custom variants visible to a stylesheet that is bundled separately — a Vue/Svelte/Astro <style> block, or a CSS module — without emitting any CSS.

Syntax
@reference "path";
@reference "tailwindcss";
Card.vue
<template>
  <h1>Hello world!</h1>
</template>

<style>
  @reference "../../app.css";

  h1 {
    @apply text-2xl font-bold text-red-500;
  }
</style>

If your theme is unmodified, @reference "tailwindcss"; is enough.

@config#

Compatibility Loads a legacy JavaScript configuration file. v4 does not read tailwind.config.js unless you opt in with this directive.

Syntax
@config "path/to/tailwind.config.js";
CSS
@import "tailwindcss";
@config "../../tailwind.config.js";

Use it while migrating from v3, or when a dependency's config must keep working. Not everything carries over — corePlugins, safelist and separator are not supported in v4. Use @source inline() in place of safelist.

@plugin#

Compatibility Loads a JavaScript plugin, by package name or by path.

Syntax
@plugin "name";
@plugin "name" { option: value; }
CSS
@import "tailwindcss";

@plugin "@tailwindcss/typography";

@plugin "@tailwindcss/forms" {
  strategy: "class";
}

The optional block passes options to the plugin — the CSS equivalent of the object you would have passed in tailwind.config.js. See Plugins.

@layer#

v4 uses native CSS cascade layers, in this order:

CSS
@layer theme, base, components, utilities;

Anything not in a layer sits in the implicit layer, which comes last and wins. @layer in v4 is the real CSS at-rule — it no longer registers utilities the way it did in v3.

Syntax
@layer base { … }
@layer components { … }
CSS
@import "tailwindcss";

/* Element defaults — every utility can override these */
@layer base {
  h1 {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-bold);
  }
}

/* Opinionated component defaults — utilities still win */
@layer components {
  .card {
    background-color: var(--color-white);
    border-radius: var(--radius-lg);
    padding: --spacing(6);
  }
}

--alpha()#

Adjusts the opacity of a colour. This is the function the /50 opacity modifier uses internally.

Syntax
--alpha(color / percentage)
CSS
.my-element {
  color: --alpha(var(--color-lime-300) / 50%);
}
Compiled
.my-element {
  color: color-mix(in oklab, var(--color-lime-300) 50%, transparent);
}
Loading preview…

In markup the opacity modifier does this for you; --alpha() is for hand-written CSS.

--spacing()#

Multiplies the spacing scale's base unit, so hand-written CSS stays on the same rhythm as your utilities.

Syntax
--spacing(number)
CSS
.my-element {
  margin: --spacing(4);
}
Compiled
.my-element {
  margin: calc(var(--spacing) * 4);
}

It works inside arbitrary values too:

HTML
<div class="py-[calc(--spacing(4)-1px)]">…</div>

--theme()#

Reads a theme value in a context where CSS custom properties are not allowed — most importantly, inside a media query condition.

Syntax
--theme(--variable-name)
CSS
/* var(--breakpoint-lg) is invalid here; --theme() is resolved at build time */
@media (width >= --theme(--breakpoint-lg)) {
  .sidebar {
    display: block;
  }
}

Everywhere else, plain var(--breakpoint-lg) is the right tool — it stays live, and --theme() does not.

The legacy theme() function#

Deprecated Dot-notation access to theme values, carried over from v3.

Syntax
theme(path.to.value)
CSS
.my-element {
  margin: theme(spacing.12);
}

It still resolves, so v3 stylesheets keep building. In new code use the CSS variable directly — var(--spacing), var(--color-red-500) — or --spacing() and --theme().

Common mistakes#

  • Rules before @import. Every @import must be at the top of the file. This is a CSS rule, not a Tailwind one.
  • @layer utilities for custom utilities. Use @utility — the v3 behaviour is gone.
  • @apply in a scoped <style> block with no @reference. The utility names are not in scope, so the build fails.
  • @import "tailwindcss" inside a component. Use @reference.
  • Expecting @config to restore everything. corePlugins, safelist and separator have no v4 equivalent through that path.
  • var() inside a media query. CSS does not allow it. Use --theme().
  • Reaching for theme() in new code. It is deprecated; theme values are ordinary CSS variables now.