Tailwind CSS Custom Utilities with @utility in v4
AI generated
</>
tw
Tailwind CSS · @utility · Custom Utilities · v4 · CSS-First
Tailwind CSS Custom Utilities
@utility in v4, no JavaScript plugins

Custom utility classes are part of everyday life in every Tailwind project. With the new @utility directive in Tailwind CSS v4, custom utilities become first-class utilities: full variant support, correct cascade placement, and not a single line of JavaScript required anymore.

11 min read @utility · @layer utilities · Variants · Responsive · CSS-First Tailwind CSS v4.0+

1. Why custom utilities are needed in Tailwind CSS

Tailwind's built-in utility set covers the vast majority of CSS properties, but every real project has specific requirements that the standard utilities do not fully cover. These can be CSS properties Tailwind has not built in, such as content-visibility, scroll-snap-type, or proprietary properties for specific browser features. They can also be project-specific combination utilities that bundle several CSS declarations that frequently occur together. In every larger Tailwind CSS project, a collection of such custom extensions accumulates over time.

In Tailwind CSS v3 there were mainly three ways to do this: the @layer utilities pattern for CSS extensions, the plugin system with the addUtilities() API for programmatic utilities, and arbitrary values with square brackets like content-visibility-[auto] for one-off values. Each of these ways had drawbacks: @layer utilities does not deliver full variant support the way built-in utilities do, the plugin API requires JavaScript and an understanding of the plugin architecture, and arbitrary values are unwieldy for frequently used values. Tailwind CSS v4 solves this problem elegantly with the @utility directive.

The @utility directive in Tailwind CSS v4 is the direct replacement for all three approaches in one. It makes it possible to define CSS utilities directly in CSS that behave like built-in Tailwind utilities, with full variant support, correct cascade placement, and without JavaScript. That turns custom utilities into a first-class concept in the framework, not a workaround or an extension outside the main system.

2. @utility: structure and first examples

The syntax of the @utility directive in Tailwind CSS v4 is refreshingly simple: @utility followed by the class name, then a CSS block with the declarations. The class name should follow Tailwind's naming convention, kebab-case, descriptive and short. In the HTML, the class is used without the @utility prefix, exactly like any other Tailwind utility class.

An important difference from working with regular CSS: inside the @utility block you only define the CSS declarations, not the selector. Tailwind CSS takes care of selector generation based on the class name. The framework generates the CSS selector from it (.utility-name { ... }) and makes sure the class is placed in the correct cascade layer. This approach differs fundamentally from @layer utilities { .my-class { ... } }, where you write the selectors yourself.


/* app.css: Custom utilities with @utility in Tailwind CSS v4 */
@import "tailwindcss";

/* Simple single-property utilities */
@utility content-auto {
  content-visibility: auto;
}

@utility content-hidden {
  content-visibility: hidden;
}

@utility snap-x-mandatory {
  scroll-snap-type: x mandatory;
}

@utility snap-y-proximity {
  scroll-snap-type: y proximity;
}

@utility snap-start {
  scroll-snap-align: start;
}

/* Multi-property utility, shorthand for common combinations */
@utility gpu-accelerate {
  transform: translateZ(0);
  will-change: transform;
  backface-visibility: hidden;
}

/* Utility using @theme tokens, references CSS custom properties */
@utility card-surface {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

/* Usage: <div class="content-auto sm:snap-x-mandatory hover:gpu-accelerate card-surface"> */

The result in the generated CSS is a class cleanly placed in the utilities layer: .content-auto { content-visibility: auto; }. The custom utility behaves identically to a built-in Tailwind utility as far as the browser is concerned, which matters for linting tools, DevTools, and CSS analysis tools.

3. Full variant support for custom utilities

The decisive advantage of the @utility directive in Tailwind CSS v4 over every previous approach is automatic, full variant support. Any class defined with @utility can immediately be combined with all built-in variants: hover:content-auto, sm:snap-x-mandatory, dark:card-surface, focus-within:gpu-accelerate. You do not need to declare these combinations explicitly, Tailwind generates them on demand, exactly as it does for built-in utilities.

That is a fundamental difference from @layer utilities classes in Tailwind CSS v3. Classes in the @layer utilities block of v3 did receive variant support for commonly used variants, but not the full, dynamic combinability of every variant. With Tailwind CSS v4 @utility, the rule is: if a variant exists in Tailwind, whether built in or defined via @variant, it can be combined with any @utility class. Custom variants and custom utilities are fully interoperable.

4. @utility vs. @layer utilities: the decisive difference

Many developers wonder, when getting started with Tailwind CSS v4, what the concrete difference is between @utility my-class { ... } and @layer utilities { .my-class { ... } }, since both approaches seemingly deliver similar results. The difference lies in registration with the framework. @utility classes are registered by Tailwind CSS as framework utilities, the framework knows they exist, can generate variant classes for them, and takes them into account in scan results. @layer utilities classes are regular CSS placed in a particular layer, but the framework does not treat them as its own utilities.

The practical consequence: for classes meant to be used in HTML with variants (hover:content-auto, sm:card-surface), @utility is the correct choice in Tailwind CSS v4. For classes used only without variants, or for classes that must be defined through direct CSS selectors (such as pseudo-element styles or complex CSS combinators), @layer utilities remains the right path. In practice, @utility is the better choice for the majority of custom utility definitions in modern Tailwind projects.


/* Comparison: @utility vs @layer utilities in Tailwind CSS v4 */

/* === @utility: Tailwind registers this as a first-class utility ===
   Supports ALL variants: hover:, sm:, dark:, focus:, group-hover:, etc.
   Correct cascade layer placement is automatic */
@utility text-balance {
  text-wrap: balance;
}
/* HTML: <h2 class="text-balance sm:text-balance lg:text-pretty"> */

/* === @layer utilities: regular CSS in the utilities layer ===
   NOT registered as a Tailwind utility.
   Variant combinations must be written manually if needed.
   Best for: classes needing complex selectors or pseudo-elements */
@layer utilities {
  .text-shadow-sm {
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  }

  /* Complex selector, cannot be done in @utility */
  .prose-custom > * + * {
    margin-top: 1.5rem;
  }

  /* Pseudo-element, @utility doesn't support & nesting in all cases */
  .scrollbar-hide::-webkit-scrollbar {
    display: none;
  }
  .scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
}

5. Real-world examples of custom utilities

In practice, Tailwind CSS custom utilities tend to arise for three categories: CSS properties missing from the built-in Tailwind set; layout utilities for complex CSS grid or flexbox patterns; and performance utilities for specific browser hints. All three categories benefit massively from the @utility approach in Tailwind CSS v4, because the resulting classes combine seamlessly with responsive prefixes and state variants.

For Magento Hyvä projects, which likewise rely on Tailwind CSS, custom utilities are particularly valuable for shop-specific UI patterns: product list layouts, specific scroll behavior for horizontal category navigation, or print styles for order confirmations. In every one of these cases, combinability with responsive variants is the decisive advantage, a sm:scroll-snap-product-grid instead of a complex responsive CSS block.

6. Responsive and state variants with custom utilities

Responsive design with Tailwind CSS custom utilities in v4 is exactly as simple as with built-in utilities. If you define a utility @utility masonry-grid { ... }, then sm:masonry-grid, md:masonry-grid, lg:masonry-grid, and xl:masonry-grid are automatically available, Tailwind generates these classes on demand whenever they are found in the HTML scan. That enables fully responsive custom utilities without manual media query definitions for every class.

State variants work just as seamlessly. hover:gpu-accelerate, focus-within:content-auto, group-hover:snap-x-mandatory, all of these combinations are supported by Tailwind CSS v4 for every class defined with @utility. That is the core of the first-class utility concept: you define the CSS property once, and the framework takes care of every context in which it can be used. The same applies to custom variant utilities defined with @variant, they too are combinable with every @utility class.


/* Real-world @utility examples for Magento / e-commerce projects */
@import "tailwindcss";

@theme {
  --color-sale: #dc2626;
  --color-new:  #16a34a;
  --color-out:  #9ca3af;
}

/* Product grid layout utility */
@utility product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
  gap: var(--spacing-6);
}

/* Price badge utilities */
@utility badge-sale {
  background-color: var(--color-sale);
  color: white;
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 9999px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Smooth image lazy-load reveal */
@utility img-reveal {
  opacity: 0;
  transition: opacity 0.4s ease;
}

@utility img-reveal-loaded {
  opacity: 1;
}

/* Truncate to N lines with ellipsis */
@utility line-clamp-2 {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

/* Usage: <div class="product-grid sm:product-grid lg:product-grid">
          <span class="badge-sale hover:badge-sale">Sale</span>
          <p class="line-clamp-2 sm:line-clamp-3"> */

7. Migrating from addUtilities() plugins to @utility

The plugin system of Tailwind CSS v3 with the addUtilities() API was the primary way to add custom utilities programmatically. In Tailwind CSS v4, most of these plugins can be replaced by simple @utility declarations in CSS. The migration follows a clear pattern: every entry in the addUtilities() object becomes its own @utility block. The class name (without the leading dot) becomes the @utility name, the CSS properties stay identical.

Limits of the @utility migration: plugins that generate utilities dynamically based on @theme values, for example a plugin that creates a custom badge utility for every color in the theme, cannot be translated directly into static @utility blocks. In Tailwind CSS v4 you keep such dynamic plugins with the @plugin directive, or you solve the problem through token-based CSS custom properties that the built-in Tailwind engine processes directly.

8. Custom utility strategies compared

A direct comparison of every approach to extending Tailwind CSS with custom utilities shows which approach is optimal for which use case.

Approach Variant support JavaScript required Best used for
@utility (v4) Full (all variants) No Standard for new custom utilities
@layer utilities Limited No Complex selectors, pseudo-elements
addUtilities() plugin (v3) Full Yes Legacy, replace with @utility
Arbitrary values [...] Full No One-off values, no fixed name
@plugin (v4) Full Yes Generated dynamically from @theme tokens

Tailwind CSS v4 @utility is the clear winner for the majority of custom utility use cases: full variant support without JavaScript. The other approaches remain relevant for their specific niches, arbitrary values for truly one-off values, @layer utilities for complex selectors, and @plugin for programmatically generated utility sets.

9. Combining @utility with @theme

The true strength of the CSS-first approach in Tailwind CSS v4 shows itself in the combination of @utility and @theme. Since @theme tokens are available as CSS custom properties on :root, @utility definitions can reference these tokens directly. That means: change a token value in @theme, and every @utility class using that token updates automatically. No manual search and replace of values in custom utilities required.

This pattern is especially valuable for project-wide design consistency. If the card background color is defined centrally in the @theme token --color-surface, and every card-like custom utility references that token via background-color: var(--color-surface), a single change in @theme is enough to update the entire design. That is the design token system in its most effective form: tokens in @theme, utilities in @utility, everything in CSS without JavaScript indirection.


/* @utility combined with @theme tokens, single source of truth */
@import "tailwindcss";

@theme {
  /* Design tokens, one place to change, everywhere updated */
  --color-surface:       #ffffff;
  --color-surface-alt:   #f8fafc;
  --color-border:        #e2e8f0;
  --color-text-muted:    #64748b;
  --radius-card:         1rem;
  --shadow-card:         0 1px 3px rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);
  --spacing-card:        1.5rem;
}

/* Custom utilities referencing @theme tokens */
@utility card {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--spacing-card);
}

@utility card-compact {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: calc(var(--radius-card) / 2);
  padding: calc(var(--spacing-card) / 2);
}

@utility card-alt {
  background-color: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

/* Any @theme token change propagates to all @utility usages automatically */
/* Usage: <div class="card hover:shadow-lg sm:card-compact dark:card-alt"> */

10. Summary

The @utility directive in Tailwind CSS v4 is the most elegant solution for custom utilities the framework has offered so far. It makes the addUtilities() plugin unnecessary for most use cases, delivers full variant support without extra configuration, and integrates seamlessly into the framework's CSS-first approach. With @utility, custom utilities really are first-class utilities, not exceptions or workarounds.

In day-to-day work, that means: instead of writing and configuring a JavaScript plugin file, you write CSS. Instead of understanding a plugin API, you write CSS. Instead of jumping back and forth between JavaScript configuration and CSS output, you write CSS. That is the core message of the Tailwind CSS v4 CSS-first approach, and @utility is its most concise expression in everyday development work.

Tailwind CSS @utility in v4, the essentials at a glance

First-class utilities

@utility registers classes as framework utilities, full variant support (hover:, sm:, dark:, group-hover:) automatically.

No JavaScript

Fully replaces addUtilities() plugins for static custom utilities. Directly in CSS without plugin API or tailwind.config.js.

@theme integration

@utility classes reference @theme tokens via var(). Token changes propagate automatically into every custom utility.

vs. @layer utilities

@utility for classes needing variant support. @layer utilities for complex selectors and pseudo-elements. Both have their place.