Custom Utilities
Create custom utilities with @utility, custom variants with @variant, and use plugins in Tailwind CSS v4.
Search across all documentation pages
Create custom utilities with @utility, custom variants with @variant, and use plugins in Tailwind CSS v4.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
/* globals.css */
@import "tailwindcss";
/* Custom utility - generates a single class */
@utility container-prose {
max-width: 65ch;
margin-inline: auto;
padding-inline: 1.5rem;
}
/* Custom utility with responsive/hover support (automatic) */
@utility text-balance {
text-wrap: balance;
}
/* Custom variant */
@variant hocus (&:hover, &:focus-visible);
@variant scrolled (&:where([data-scrolled]));
/* Using a v3-style plugin via @config */
@config "./tailwind.config.js";// Usage
<div className="container-prose">
<h1 className="text-balance hocus:text-blue-600">Custom Utilities</h1>
</div>
<header data-scrolled className="scrolled:bg-white scrolled:shadow">When to reach for this: When Tailwind's built-in utilities do not cover a specific pattern you use repeatedly - create a custom utility once and use it everywhere.
/* globals.css */
@import "tailwindcss";
/* Layout utilities */
@utility stack {
display: flex;
flex-direction: column;
}
@utility center {
display: flex;
align-items: center;
justify-content: center;
}
@utility container-narrow {
max-width: 42rem;
margin-inline: auto;
padding-inline: 1rem;
}
/* Visual utilities */
@utility glass {
background: rgb(255 255 255 / 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgb(255 255 255 / 0.2);
}
@utility gradient-text {
background: linear-gradient(to right, var(--color-primary), var(--color-secondary, #8b5cf6));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
@utility scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
/* Interactive variants */
@variant hocus (&:hover, &:focus-visible);
@variant group-hocus (:merge(.group):hover &, :merge(.group):focus-visible &);
@variant aria-current (&[aria-current="page"]);
@variant keyboard-focus (&:focus-visible);
/* State variants */
@variant loading (&[data-loading]);
@variant empty-state (&:where(:empty, [data-empty]));
@theme {
--color-primary: #2563eb;
}export function CustomUtilitiesDemo() {
return (
<div className="container-narrow stack gap-8 py-12">
<h1 className="gradient-text text-4xl font-bold">
Custom Utilities in Action
</h1>
<nav className="glass sticky top-0 z-10 rounded-xl p-4">
<ul className="flex gap-4">
<li>
<a href="#" className="rounded px-3 py-1 hocus:bg-gray-100 aria-current:font-bold" aria-current="page">
Home
</a>
</li>
<li>
<a href="#" className="rounded px-3 py-1 hocus:bg-gray-100">About</a>
</li>
<li>
<a href="#" className="rounded px-3 py-1 hocus:bg-gray-100">Contact</a>
</li>
</ul>
</nav>
<div className="scrollbar-hidden flex gap-4 overflow-x-auto pb-2">
{Array.from({ length: 10 }, (_, i) => (
<div key={i} className="shrink-0 center size-24 rounded-lg bg-gray-100 text-sm font-medium">
Card {i + 1}
</div>
))}
</div>
<button
data-loading
className="rounded bg-blue-600 px-4 py-2 text-white loading:opacity-50 loading:cursor-wait"
>
Submit
</button>
</div>
);
}What this demonstrates:
@utility for reusable composite utility classes@variant for custom state/pseudo selectorsaria-current, data-loading variants@utility name { ... } creates a utility class .name in the utilities layermd:name), state (hover:name), and other modifiers@variant name (selector) creates a modifier that applies when the selector matches@variant hocus (&:hover, &:focus-visible)& in variant selectors represents the element the variant is applied to@layer components in v3Functional utility (with values) via @theme:
/* You cannot create arbitrary-value custom utilities with @utility.
Instead, define theme values and use built-in utilities. */
@theme {
--spacing-page: 2rem;
--spacing-section: 4rem;
--width-content: 65ch;
--width-wide: 90rem;
}
/* Now use: p-page, gap-section, max-w-content, max-w-wide */Using v3 plugins:
// tailwind.config.js (only needed for plugins)
export default {
plugins: [
require("@tailwindcss/typography"),
require("@tailwindcss/forms"),
],
};/* globals.css */
@import "tailwindcss";
@config "./tailwind.config.js";
/* Now prose and form-* classes are available */Combining with CSS nesting:
@utility card {
border-radius: 0.75rem;
border: 1px solid var(--color-border);
padding: 1.5rem;
background: var(--color-surface);
& > h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
& > p {
color: var(--color-muted-foreground);
}
}Complex variant selectors:
/* Open state (for details/dialog) */
@variant open (&[open], &[data-state="open"]);
/* Parent state variant */
@variant sidebar-open (:merge(.sidebar-open) &);
/* Print variant is built-in, but custom media: */
@variant portrait (@media (orientation: portrait));
@variant touch (@media (pointer: coarse));// Custom utilities are CSS - no TS impact
// But you can create a utility map for documentation:
const customUtilities = {
"container-prose": "max-width: 65ch centered with inline padding",
"glass": "frosted glass background with backdrop blur",
"gradient-text": "gradient fill on text",
"scrollbar-hidden": "hides scrollbar across all browsers",
} as const;
type CustomUtility = keyof typeof customUtilities;@utility names must be single identifiers - No dots, colons, or spaces. @utility my-card works; @utility my.card does not.
Custom utilities cannot take arguments - @utility spacing($value) does not exist. Fix: Define theme values and use built-in utilities (p-page, gap-section).
Plugin compatibility - Not all v3 plugins work with v4 yet. Fix: Check plugin documentation for v4 support, or replicate the plugin's CSS with @utility.
@config for plugins only - Using @config with a full v3 config may conflict with @theme. Fix: Only put plugin registration in the JS config; move all theme values to @theme.
Variant specificity - @variant uses the selector you provide. If your selector has high specificity, it may override other styles unexpectedly. Fix: Use :where() to zero out specificity where needed.
| Alternative | Use When | Don't Use When |
|---|---|---|
@apply | You want to compose existing Tailwind utilities into a class | You can use @utility directly (cleaner) |
| Component abstraction | The pattern includes HTML structure, not just styles | You only need a CSS class |
| Tailwind plugins (JS) | You need dynamic utility generation with values | A static @utility covers your need |
| PostCSS plugins | You need transformations beyond Tailwind's scope | Tailwind's built-in features suffice |
@utility name { ... } creates a new utility class in the utilities layer@apply composes existing Tailwind utilities into a class@utility is cleaner and purpose-built for custom utilities in v4Yes. A utility defined with @utility automatically works with md:, hover:, dark:, and all other modifiers.
@variant hocus (&:hover, &:focus-visible);Usage: <a className="hocus:text-blue-600">Link</a>
No. @utility cannot take arguments. Instead, define theme values and use built-in utilities:
@theme {
--spacing-page: 2rem;
}
/* Now use p-page, gap-page, etc. */@import "tailwindcss";
@config "./tailwind.config.js";Only put plugin registration in the JS config. Keep all theme values in @theme.
@utility card {
border-radius: 0.75rem;
padding: 1.5rem;
& > h2 {
font-size: 1.25rem;
font-weight: 600;
}
}Yes, native CSS nesting is supported inside @utility.
Names must be single identifiers. Hyphens are allowed (my-card), but dots, colons, and spaces are not (my.card will fail).
@variant loading (&[data-loading]);Usage: <button data-loading className="loading:opacity-50">
The variant selector has high specificity. Fix by using :where() to zero out specificity: @variant loading (&:where([data-loading])).
Theme values from the JS config can conflict with @theme values in CSS. Only use @config for plugin registration; move all theme values to @theme.
const customUtilities = {
"container-prose": "max-width: 65ch centered with inline padding",
"glass": "frosted glass background with backdrop blur",
} as const;
type CustomUtility = keyof typeof customUtilities;Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥