Chapter 13 of 14 · Tailwind CSS for Beginners — Complete Free Tutorial

Reusable Components and @apply

Utility-first styling has a natural escape hatch for the moment repetition stops paying off. This chapter of the Tailwind CSS for Beginners series covers when to extract a component class, how @apply pulls utilities into your own CSS, what replaced the old @layer components in v4, and how to keep component classes working with variants such as hover and focus.

When repetition hurts

The workflow so far has been honest about the trade: writing everything in markup is fast and visible, until the same cluster of classes appears for the third time. A useful rule is the rule of three — write it inline twice, and extract on the third. Extraction buys you three things at once:

  • One place to change — restyle the button once instead of hunting through every template.
  • Shorter markup — class="btn" is easier to scan than twelve utilities you must decode each time.
  • A vocabulary for your team — components become names people can say out loud in a review.

What you lose is locality: the styling now lives somewhere else, so the habit that keeps it honest is keeping component CSS in one dedicated file and never letting it grow rules that no longer map to a real component.

@apply: utilities in your own rules

@apply lets you use utilities inside a normal CSS selector, which is exactly what extraction needs. The syntax is unchanged from v3:

@import "tailwindcss";

@layer components {
  .btn {
    @apply inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold;
  }
  .btn-primary {
    @apply bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:ring-2;
  }
  .btn-ghost {
    @apply border border-slate-300 bg-transparent text-slate-700 hover:bg-slate-100;
  }
}

Expected output:

Three class names you wrote yourself now behave like utilities:
buttons align their icons, keep even padding, and change colour on
hover and keyboard focus — with every variant already compiled.

Because the variants are inside the @apply call, the component handles its own states: no markup changes are needed when a new button appears anywhere in the project.

Three ways to define reusable CSS in v4

v4 separates two ideas that v3 blurred together — a component class that always styles an element, and a utility class that can be combined freely:

ApproachBest forNotes
@layer components { .card { … } }Complex components with several declarationsStays in cascade layer order, below utilities
@utility card-title { … }Single-purpose additions that should work with hover: and md:The v4 replacement for @layer utilities
:root or a plain ruleStyles with no utility equivalentUse theme variables for the values

The distinction matters at the moment you try hover:card — variants only compose with utilities, so anything you want to modify with a prefix belongs in @utility. Custom utilities are also sorted by how many properties they set, which lets a multi-property utility still be overridden by a simple one.

Components that must compose

Here is the same idea built as a utility instead of a component class, which keeps prefixes available:

@utility badge {
  display: inline-flex;
  align-items: center;
  border-radius: 9999px;
  padding-inline: 0.75rem;
  padding-block: 0.25rem;
  font-size: 0.75rem;
  font-weight: 500;
}
<span class="badge bg-emerald-100 text-emerald-700">Stable</span>
<span class="badge bg-amber-100 text-amber-800 hover:brightness-95">Beta</span>
<span class="badge bg-slate-100 text-slate-600 md:text-xs">Archived</span>

Expected output:

Three pill-shaped labels sharing identical shape and spacing, each
with its own tint and text colour; the middle one dims slightly on
hover and the last shrinks on wider screens.

Every class in that list still accepts a variant, because @utility registers a real utility rather than a locked-down rule. That flexibility is the whole argument for choosing it over a component layer when the thing you are extracting is small.

Working across files: @reference

One v4 detail catches people out in real applications. Frameworks such as Vue, Svelte and Astro compile <style> blocks separately from your main stylesheet, so a scoped block cannot see your theme variables, custom utilities or variants. The fix is a single import that shares definitions without duplicating CSS in the bundle:

<style>
  @reference "../../app.css";

  .panel-title {
    @apply text-lg font-semibold text-slate-900;
  }
</style>

Without @reference, @apply in a scoped style block fails and tells you exactly that. Plain projects that compile everything from one entry file never need it.

Keeping the line between utilities and components clean

Extraction is a tool, not a destination. A few guardrails keep the system healthy: extract behaviour, not decoration (a .muted class that hard-codes one grey prevents a palette change); leave one-off combinations in markup; keep variants in markup where they are visible, and only bake a state into a component when every instance needs it. When a component grows past a handful of declarations or starts needing options — sizes, colours, variants — consider a modifier class such as .btn-primary or a set of utilities layered on top, which is how most design systems stay small.

Common mistakes

  • Extracting too early — a component class invented on the first use usually encodes guesses that the second use disproves.
  • @apply inside scoped styles without @reference — the build fails until you add the import.
  • Expecting variants on component classes — hover:card does nothing; use @utility for anything that must compose.
  • Building a framework inside your framework — thirty component classes with four options each is a design system you did not mean to write.
  • Forgetting the import at the top — every file that uses these features still needs @import "tailwindcss"; first.

Practice checklist

  1. Take a repeated card from an earlier chapter and extract a .card component with @apply.
  2. Add .btn-primary and .btn-ghost variants and use both in markup.
  3. Convert one small component into an @utility and add hover: and md: prefixes to it in HTML.
  4. Check your compiled output and confirm the component rules appear in the components layer, below utilities.

Components consume your design tokens — the final chapter is about creating and shipping them: Theming and Optimizing Your Build.

Quiz: Reusable Components and @apply

5 questions · 70% to pass

Frequently Asked Questions

Tailwind is a utility-first CSS framework: instead of writing custom classes like .card-title, you compose elements from small single-purpose utilities such as text-lg font-semibold text-slate-800. The styles ship as a generated stylesheet containing only the classes your project uses, so the CSS stays small and consistent.

Bootstrap ships a opinionated component layer (buttons, navbars with Bootstrap's own look) that you override with custom CSS. Tailwind ships primitive utilities and lets you design freely — two sites built with Tailwind can look completely different, while two Bootstrap sites tend to look related. You can still build components; you just compose them from utilities first.

You need the fundamentals: the box model, specificity, Flexbox, Grid, media queries and pseudo-classes. Tailwind does not replace CSS knowledge — it renames it. Understanding what padding, margin, gap and z-index mean makes every utility class obvious instead of something to memorise.

v4 is CSS-first: configuration lives in CSS with @import "tailwindcss" and @theme tokens instead of a JavaScript tailwind.config.js, the engine is rewritten in Rust for much faster builds, and modern CSS features (container queries, cascade layers) are built in. This tutorial teaches v4 and notes where v3 differed.

Long class lists are a real trade-off, and the tutorial covers two fixes: consistent ordering (layout → spacing → typography → colour) and small component classes for repeated combinations. The generated CSS contains only classes you actually use — Tailwind removes everything else at build time, so the stylesheet is typically smaller than an unused framework.

Three utilities cover most cases: mx-auto centres a fixed-width block horizontally, flex items-center justify-centre centres children inside a flex parent, and grid place-items-center does the same for grids. Chapter 8 and chapter 9 show both patterns with examples.

Yes — Tailwind is a build step that scans your template files for class names and generates CSS, so it works with any framework or none. It does not ship runtime JavaScript, and the classes look the same in JSX, Blade, PHP or static HTML files.

In v4 you define design tokens in CSS with @theme, for example --color-brand-500: #4f46e5; creates bg-brand-500, text-brand-500 and friends. For a one-off value you can always use arbitrary values like bg-[#0f172a], which need no configuration at all.

A variant prefixes a utility with the condition it applies under: hover:bg-indigo-600 on hover, focus:ring-2 while focused, dark:bg-slate-900 in dark mode, and sm: / md: / lg: at breakpoints. Variants stack, such as md:hover:text-lg, and chapter 11 covers the full pattern.

Yes. Tailwind compiles to a static stylesheet with no JavaScript requirement, and because it ships only used classes, pages stay fast — a Core Web Vitals win that helps SEO. Accessibility, semantic markup and good content remain your job; Tailwind only styles what you write.