Custom Utilities and Variants: @utility, @custom-variant and @layer

Advanced
13 min

Custom Utilities and Variants: @utility, @custom-variant and @layer

@apply bundles existing utilities into a class. Sometimes you need the opposite: a new utility Tailwind does not ship, a variant for a state it does not know, or a default style for every <h1>. v4 provides three directives for this. After this lesson you will be able to write utilities that support every variant, define your own variants, and set base styles without fighting the cascade.

@utility for new utilities

In v4, a class inside @layer utilities is plain CSS and ignores hover: and md:. To register a real utility, use @utility:

css
@utility content-auto { content-visibility: auto; } @utility scrollbar-hidden { scrollbar-width: none; &::-webkit-scrollbar { display: none; } }

Both now behave like built-ins: md:content-auto and hover:scrollbar-hidden work, IntelliSense suggests them, and they are emitted only when used. Nesting with & and @apply both work inside @utility.

Functional utilities with --value()

A * in the name makes the utility accept a value. The --value() function decides which values are allowed:

css
@theme { --tab-size-github: 8; } @utility tab-* { tab-size: --value(--tab-size-*); /* tab-github, from the theme */ tab-size: --value(integer); /* tab-2, tab-4: bare numbers */ tab-size: --value([integer]); /* tab-[13]: arbitrary values */ }

Tailwind keeps whichever declaration resolves, so several sources form a fallback chain. Other forms include --value(percentage), --spacing(--value(number)) to reuse the spacing scale, and --modifier() to read a slash modifier. Negative versions are declared separately as @utility -inset-*.

Custom variants

@custom-variant registers a new prefix. The shorthand form takes a selector where & is the element:

css
@custom-variant theme-midnight (&:where([data-theme="midnight"], [data-theme="midnight"] *)); @custom-variant hocus (&:hover, &:focus-visible);

theme-midnight:bg-slate-950 and hocus:underline are now valid everywhere. The same directive is how class-based dark mode is enabled: @custom-variant dark (&:where(.dark, .dark *));.

The long form with @slot supports at-rules and multiple selectors:

css
@custom-variant any-hover { @media (any-hover: hover) { &:hover { @slot; } } }

@slot marks where the utility's declarations go; use this form for media and supports queries.

@variant inside custom CSS

When writing ordinary CSS rules, you can still use variants instead of repeating media queries and selectors:

css
.prose-callout { background: var(--color-amber-50); @variant dark { background: var(--color-amber-950); } @variant md { padding: --spacing(6); } }

@variant compiles to the same selector or media query the utility would use, so behaviour stays consistent.

@layer base, @layer components and Preflight

Tailwind's output is organised in cascade layers: theme, base, components, utilities. Placing your own rules in base or components guarantees that utilities still win when both apply:

css
@layer base { body { @apply bg-white text-gray-900 antialiased; } a { @apply text-blue-700 underline-offset-2; } } @layer components { .card { @apply rounded-xl border border-gray-200 bg-white p-6 shadow-sm; } }

Because .card lives in the components layer, <div class="card p-8"> gets the larger padding — utilities come later. Preflight (the reset) is part of base; to skip it, import the layers individually without preflight.css:

css
@layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/utilities.css" layer(utilities);

Common mistakes

  • Writing .my-class in @layer utilities and expecting variants to work; use @utility.
  • Naming a functional utility without the *, so tab-4 is never generated.
  • Putting component classes outside any layer, where they outrank utilities and become hard to override.
Quick Quiz
Question 1 of 2

What is the difference between a class in `@layer utilities` and one declared with `@utility` in v4?

Key Takeaways

  • @utility name { … } registers a real utility with variant and IntelliSense support; @layer utilities no longer does.
  • @utility name-* { prop: --value(…); } creates functional utilities that read theme keys, bare values or arbitrary values.
  • @custom-variant adds new prefixes, using a selector shorthand or @slot for at-rules.
  • @variant applies existing variants inside handwritten CSS rules.
  • Put base and component styles in @layer base / @layer components so utilities keep priority; import layers manually to drop Preflight.

Next lesson: Official Plugins: Typography and Forms — add @tailwindcss/typography for rich text and @tailwindcss/forms for consistent form controls.

Custom Utilities and Variants: @utility, @custom-variant and @layer - Tailwind CSS | CodeYourCraft | CodeYourCraft