Tailwind’s @apply: More Than Just a CSS Shortcut

Tailwind CSS often gets a bad rap for encouraging long, unwieldy class lists in HTML. While that criticism has some merit, the framework’s methodology offers real benefits for maintainability and performance—if you know where to look. One feature that rarely gets the attention it deserves is @apply, which is far more capable than its typical showcase suggests.

From Sass Mixins to Tailwind Utilities

At its core, @apply works like Sass’s @include for mixins: it copies a predefined set of styles into your CSS. But the key difference is that Tailwind doesn’t require you to write a custom selector to house those styles. You can use the same utility classes directly in your HTML without any extra steps.

/* Input */
.selector {
  @apply p-4;
}

/* Output */
.selector {
  padding: 1rem;
}

Consider the Sass workflow: you define a mixin with @mixin, invoke it with @include inside a selector, and only then can you apply it to markup. Tailwind utilities skip that intermediate layer entirely. The responsive variants also work straight out of the box—sm:, md:, and other breakpoint prefixes let you adjust layouts without writing a single media query in your stylesheet.

// Defining the mixin
@mixin some-mixin() {
  color: red; 
  background: blue; 
}

// Using the mixin
.selector {
  @include some-mixin(); 
}

/* Output */
.selector {
  color: red; 
  background: blue; 
}
/* Defining the utility */
@utility some-utility {
  color: red; 
  background: blue; 
}

/* Applying the utility */
.selector {
  @apply some-utility; 
}

/* Output */
.selector {
  color: red; 
  background: blue; 
}

That said, Sass mixins do hold an advantage in raw power. They support multiple variables and can leverage control flow like @if or @for loops. Tailwind utilities, at most, accept a single argument through their functional syntax. Modern CSS has largely closed this gap, though: with CSS variables and arbitrary values, you can build utilities that are just as flexible for most real-world needs.

@mixin avatar($size, $circle: false) {
  width: $size;
  height: $size;

  @if $circle {
    border-radius: math.div($size, 2);
  }
}
/* Tailwind Functional Utility */
@utility tab-* { 
  tab-size: --value(--tab-size-*);
}

A Practical Layout Example

A simple use case illustrates the point well. Suppose you want two layout utilities: vertical (flexbox with flex-direction: column) and horizontal (flexbox with flex-direction: row). Defining them is straightforward, and then you can compose them directly in HTML with responsive prefixes.

@utility horizontal {
  display: flex;
  flex-direction: row;
  gap: 1rem;
}

@utility vertical {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
<div class="vertical sm:horizontal">...</div>

In this example, sm: is Tailwind’s breakpoint variant at 640px. The result: a vertical layout on mobile, flipping to horizontal on wider screens—all without touching a CSS file.

If you prefer keeping your styles in stylesheets, @apply lets you bring those same utilities back into your CSS, mimicking the Sass mixin pattern.

<div class="your-layout">...</div> 
.your-layout {
  @apply vertical; 

  @media (width >= 640px) {
    @apply horizontal;
  }
}

Either way, the code reads in plain English. You immediately see the intent—“this is a horizontal layout on tablet”—without mentally parsing CSS property values. That readability is a significant win for long-term maintenance.

Building Reusable Grids

The same principle scales to more complex patterns. Imagine building a reusable CSS Grid utility that sets repeat and minmax declarations once, so you never have to write them by hand again.

@utility grid-simple {
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  gap: var(--gap, 1rem);
}

Now you can adjust the number of columns on the fly using arbitrary properties, overriding a CSS variable only when needed. Combined with responsive variants, this makes responsive layouts highly declarative:

<div class="grid-simple [--cols:3]"> 
  <div class="item">...</div>
  <div class="item">...</div>
  <div class="item">...</div>
</div>
<div class="grid-simple sm:[--cols:3]"> 
  <div class="item">...</div>
  <div class="item">...</div>
  <div class="item">...</div>
</div>

And of course, @apply remains an option here. If you’re wary of cramming too much logic into HTML, you can create a styled class that pulls in the grid utility, keeping your markup conventional while still avoiding repetitive CSS declarations.

.your-layout {
  @apply grid-simple; 
  @media (width >= 640px) {
    --cols: 3;
  }
}
<div class="your-layout"> ... </div>

Using @apply in this manner—treating Tailwind as a toolkit rather than a rigid system—opens up room for highly complex layouts that remain easy to read and update. The framework’s utilities are powerful on their own, but they become even more so once you allow yourself to integrate them with traditional CSS techniques.