Tailwind @apply: Good Practice or Antipattern?
AI generated
</>
tw
Tailwind CSS · @apply · CSS Architecture · Best Practices
Tailwind @apply:
Good Practice or Antipattern?

@apply is one of the most controversial features in Tailwind CSS. Condemned wholesale too often, reached for too carelessly. This article shows exactly when @apply delivers real value, when it undermines the utility-first approach, and which clean alternatives avoid the architectural trade-off.

10 min read @apply · @layer components · utility-first · CSS architecture Tailwind CSS v3 · v4

1. What @apply does and why it exists

Tailwind @apply is a CSS directive that embeds Tailwind utility classes into a class of your own. Write .btn { @apply px-4 py-2 bg-sky-600 text-white rounded-lg; } and Tailwind generates the corresponding CSS declarations directly inside the class at build time, so no HTML classes need to be managed in the browser. Tailwind shipped @apply to ease the transition from a classic CSS workflow and to support concrete cases where utility classes cannot, or should not, be applied directly in the HTML.

The most important use case @apply was designed for: content that comes from a CMS or a Markdown renderer and has no access to the HTML. When a blog post renders <p>, <h2> and <ul> elements without classes, you cannot set those styles via utility classes in the markup, and this is where @apply steps in inside the CSS. That is the original idea and, to this day, the most legitimate use of Tailwind @apply.

2. Legitimate use cases: when @apply makes sense

There is a handful of situations in which Tailwind @apply delivers real value. First: CMS-generated HTML content with no control over classes, such as blog copy, product descriptions, or imported content. Second: third-party libraries that ship their own class structures and get adapted to your design system via @apply. Third: reused CSS classes for elements that cannot be modeled as a template partial, for example when a CMS-side shortcode system outputs classes you cannot control directly.

Fourth, and this is often underestimated: Tailwind @apply is legitimate for base styles you define in @layer base, such as focus-ring styles for :focus-visible, default typographic values for content elements, or reset overrides. These live sensibly in CSS, not in markup. The key test: can the element receive classes in HTML at all? If not, @apply is the cleanest solution. If it can, it is almost always the worse choice.


/* LEGITIMATE: base styles for CMS-generated content without class control */
@layer base {
  /* Focus ring for all interactive elements, cannot be set per-element in HTML */
  *:focus-visible {
    @apply outline-none ring-2 ring-sky-500 ring-offset-2;
  }

  /* Prose base styles for CMS content without class access */
  .cms-content h2 {
    @apply text-2xl font-bold text-slate-900 mt-8 mb-4;
  }
  .cms-content p {
    @apply text-base text-slate-700 leading-relaxed mb-4;
  }
  .cms-content ul {
    @apply list-disc list-inside space-y-1 text-slate-700 mb-4;
  }
  .cms-content a {
    @apply text-sky-700 underline hover:text-sky-900 transition-colors;
  }
}

/* LEGITIMATE: adapting a third-party plugin's classes to your design system */
@layer components {
  /* Flatpickr date picker, no control over generated HTML */
  .flatpickr-day.selected {
    @apply bg-sky-600 text-white border-sky-600;
  }
  .flatpickr-day:hover {
    @apply bg-sky-50 border-sky-200;
  }
}

3. The @apply antipattern: when it hurts

The @apply antipattern occurs when developers use @apply to write conventional CSS again inside a Tailwind CSS project, just with Tailwind syntax. The most common form: a CSS class .btn-primary is created with @apply, then class="btn-primary" is used in the HTML. That sounds like an optimization, but it brings several downsides. First, you lose explicit visibility: reading the HTML no longer tells you which styles the element has, you have to switch to the CSS file. Second, it undermines the responsive and state system: classes such as hover:bg-sky-700 or lg:px-6 cannot be used inside @apply, so you end up writing manual CSS again.

Third, a hidden dependency emerges: change the design token for bg-sky-600 in the configuration, and the change propagates into every @apply spot, but only there, not into ordinary CSS declarations sitting right next to them. That leads to inconsistent design tokens. Fourth, Tailwind @apply inflates the CSS output: every @apply class produces its own separate set of declarations, which gets duplicated across many uses of the same pattern instead of living once in the stylesheet and being referenced repeatedly.


/* ANTIPATTERN: @apply to recreate BEM/OOCSS in a utility-first project */

/* Wrong: this re-creates traditional CSS architecture in Tailwind */
.btn { @apply inline-flex items-center justify-center font-medium rounded-lg; }
.btn-primary { @apply bg-sky-600 text-white px-4 py-2; }
.btn-secondary { @apply bg-slate-100 text-slate-700 px-4 py-2; }

/* Now hover states require manual CSS, @apply doesn't support responsive/state */
.btn-primary:hover { background-color: #0369a1; } /* hardcoded value, not a token */

/* Wrong: this bypasses Tailwind's purge and outputs unused CSS */
.card { @apply rounded-xl border border-slate-200 p-6 shadow-sm; }
.card-header { @apply font-bold text-slate-900 text-lg mb-2; }

/* ---- CORRECT: Keep classes in HTML, no @apply needed ---- */
/*
<div class="rounded-xl border border-slate-200 p-6 shadow-sm">
  <h3 class="font-bold text-slate-900 text-lg mb-2">Title</h3>
</div>
*/

/* ---- CORRECT for reuse: use a template partial, not CSS abstraction ---- */
/*
{{> card title="Title" body="Content"}}
*/

4. @layer components as the cleaner alternative

If you still want to create a CSS class that bundles Tailwind styles, @layer components with @apply is the correct form, not because it fixes the antipattern, but because it anchors specificity correctly within Tailwind's layer system. Without @layer, your own CSS classes can override Tailwind utilities or be overridden by them depending on the order in the stylesheet. With @layer components, Tailwind ensures that utility classes in the HTML always win, which is the expected behavior.

Important: Tailwind @apply in @layer components does not solve the underlying antipattern. It merely integrates the CSS class correctly into the layer system. The real question remains: does this style genuinely need to live in CSS? Or can it sit directly in the HTML? For true repetition of exactly the same classes across many elements, a template-partial solution is almost always the superior abstraction.

5. Template partials: the right abstraction for repetition

The strongest argument against the @apply antipattern is the template partial. In modern frontend frameworks, PHP template engines (Twig, Blade), component frameworks (React, Vue), Web Components, the abstraction for repetition is the component, not the CSS class. A button partial in Twig, a button component in Vue, a card template in Blade: each encapsulates HTML structure and Tailwind classes together. The Tailwind classes stay in the HTML, and the abstraction sits at the right level.

Concretely: instead of defining .btn-primary { @apply … } in CSS, you write a template partial button.html.twig that contains the full Tailwind classes and can be varied via parameters. The result: full Tailwind tooling support (IntelliSense, JIT, tree-shaking), explicit visibility of styles in the markup, no duplicate CSS output. That is the method the Tailwind team itself recommends, and it avoids all the downsides of Tailwind @apply used as an antipattern.


<!-- RIGHT: Button as a template partial, all Tailwind classes in HTML, no @apply -->

<!-- button.html.twig (Twig partial) -->
<button
  type="{{ type|default('button') }}"
  class="inline-flex items-center justify-center gap-2 font-semibold rounded-xl
         px-5 py-2.5 text-sm transition-colors duration-150
         {{ variant == 'primary'
            ? 'bg-sky-600 text-white hover:bg-sky-700 focus-visible:ring-sky-500'
            : 'bg-slate-100 text-slate-700 hover:bg-slate-200 focus-visible:ring-slate-400' }}
         focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
         disabled:opacity-50 disabled:pointer-events-none"
  {{ disabled ? 'disabled' : '' }}
>
  {% if icon %}<span class="w-4 h-4">{{ icon|raw }}</span>{% endif %}
  {{ label }}
</button>

<!-- Usage -->
{% include 'button.html.twig' with { label: 'Save', variant: 'primary' } %}
{% include 'button.html.twig' with { label: 'Cancel', variant: 'secondary' } %}

6. The prose plugin: @apply for content text

The @tailwindcss/typography plugin and its prose class are a perfect example of how the Tailwind developers themselves use @apply: internally, to style generated HTML elements that have no classes. The plugin writes styles for h1 through h6, p, ul, ol, blockquote, code and other elements coming from Markdown, a CMS, or rich-text editors. The styles are fully customizable through the theme.extend.typography config object.

Anyone who does not use the prose plugin but has similar requirements can legitimately use Tailwind @apply in an @layer components block for a custom .prose-custom selector. That is the canonical use case: style targets that cannot carry classes in the HTML. Custom prose variants for different content types, such as .prose-blog, .prose-product, .prose-legal, are a clean architecture for projects with many different content sources.

7. Tailwind v4: @apply and the CSS-first approach

Tailwind CSS v4 does not change the availability of @apply, the directive remains fully in place. What changes is the context: in v4, the entire configuration lives in CSS, not in JavaScript. @layer base, @layer components and @layer utilities are standard CSS layers you control directly in the CSS file. Tailwind @apply works identically in v4, but benefits from the new CSS custom-property system: design tokens are CSS variables, not JavaScript objects, which makes @apply abstractions more transparent because the values are directly visible in the browser inspector.

In v4, @utility is a new alternative to @apply in @layer components: it registers your own utility class directly. @utility btn-primary { background-color: var(--color-sky-600); color: white; } produces a utility class that behaves exactly like Tailwind's built-in classes, with responsive modifiers, state modifiers, and correct specificity. In v4, this is the preferred alternative to Tailwind @apply for frequently used patterns.

8. Decision tree: use @apply or not?

A clear decision tree helps apply Tailwind @apply correctly. Step 1: can the element receive classes directly in HTML? If not (CMS content, third-party HTML, generated markup), then @apply is legitimate. If yes, move on to step 2. Step 2: is the element genuine repetition that should be abstracted? If yes, create a template partial, no @apply. If no, keep the classes directly in the HTML. Step 3: should it become a reusable utility class? If yes, use @utility in v4 or @layer utilities in v3, not @apply in @layer components.

This decision tree covers the most common missteps when applying Tailwind @apply. The most frequent mistake: skipping step 1 and jumping straight to the abstraction because you are used to CSS classes. The second most frequent misjudgment: replacing template partials with @apply classes because it seems "less work", when in practice it turns the CSS files back into conventional stylesheets, just with Tailwind syntax.

9. Comparison: @apply vs. alternatives

The decision for or against Tailwind @apply is ultimately an architectural decision. The following table compares the approaches by the criteria that matter in practice.

Approach Visibility State/Responsive Recommendation
Classes directly in HTML Very high Full Always prefer
Template partial High Full For repetition
@apply in @layer base Medium Limited Only for CMS content
@apply in @layer components Low Limited Avoid antipattern
@utility (Tailwind v4) High Full v4 alternative to @apply

The table makes it clear: Tailwind @apply in @layer components is the only approach that restricts both visibility and state/responsive support. It is the combination of these two downsides that makes the antipattern so problematic. In every other case, HTML classes, template partials, or v4 utilities, you retain full control over the Tailwind system.

Mironsoft

Tailwind CSS architecture, Hyva Themes and scalable frontend systems

Tailwind CSS architecture for your project?

We analyze existing Tailwind CSS codebases for @apply antipatterns, replace them with clean template partials, and establish a scalable CSS architecture.

Code audit

Analysis of CSS architecture for @apply antipatterns and improvement potential

Refactoring

Replacing @apply classes with template partials and correct layer usage

Tailwind v4 migration

From JS config to a CSS-first approach, @utility instead of @apply for custom patterns

10. Summary

Tailwind @apply is not a blanket antipattern, it is a tool with a clear scope. It is legitimate for CMS-generated content without class control, for third-party libraries, and for base styles in @layer base. It becomes an antipattern when it is used to recreate conventional CSS in Tailwind projects: replacing .btn-primary { @apply … } in HTML with class="btn-primary", sacrificing visibility and state control in the process. The alternative is the template partial, not a CSS class.

The most important principle: the abstraction for repetition in Tailwind CSS sits at the template level, not the CSS level. Tailwind v4 reinforces this approach with @utility as its own registration form for utilities that remain fully within the Tailwind system. Anyone who reaches for @apply only where HTML classes are not possible, and uses template partials for everything else, gets the best of both worlds: the expressiveness of Tailwind and the reusability of cleanly structured components.

Tailwind @apply: the essentials at a glance

Legitimate use

CMS content without class control, third-party HTML, base styles in @layer base, whenever there is no access to the HTML markup.

The antipattern

@apply in @layer components for button, card, or badge classes, losing state/responsive support and markup visibility.

The right alternative

Template partials for repeated HTML plus classes. @utility in Tailwind v4 for custom utilities in the correct system layer.

Tailwind v4

@apply remains available. New: @utility as a cleaner alternative for custom classes with full modifier support.

11. FAQ: Tailwind @apply

1Is @apply deprecated?
No, @apply is fully supported in v3 and v4. The Tailwind team has only clarified that template partials for repetition are the preferred abstraction.
2Why no hover: inside @apply?
Responsive and state modifiers do not work inside @apply, a design limit. Anyone who needs them must write CSS pseudo-selectors separately or keep the classes directly in the HTML.
3@apply vs. @layer components?
@apply bundles classes, @layer components controls specificity. Together: @layer components { .btn { @apply … } }, layer for specificity, @apply for class bundling.
4When does @apply in @layer base make sense?
For :focus-visible styles, default typographic values for CMS HTML, reset overrides, anything that cannot be set via a class in the HTML.
5Best alternative for repeated buttons?
A template partial with the full button HTML and all Tailwind classes, varied by parameters. State/responsive modifiers work, and IntelliSense works correctly.
6Why does @apply inflate the CSS?
Every @apply usage produces a copy of the declarations in the class. Tailwind utilities themselves appear only once in the stylesheet, no matter how often they occur in the HTML.
7What is @utility in Tailwind v4?
Registers a custom utility class within the Tailwind system, with full responsive and state modifier support. Replaces the @apply-in-@layer-components use case in v4 projects.
8Styling third-party components with Tailwind?
With @apply in @layer components for the library classes. Legitimate because there is no access to the library's template. Alternatively CSS custom properties from the Tailwind token system.
9Do I lose JIT benefits with @apply?
No, @apply is resolved at build time. But classes inside @apply count as used and are not purged, even if the CSS class never appears in the HTML.
10Refactor existing @apply classes?
Yes, priority for classes in @layer components used for UI components. Leave @apply in @layer base for CMS styles alone. Start with classes that need many state/responsive adjustments.