Overview
Repeating the same 6-7 utility classes on every button in an app works, but it gets unwieldy fast — change the brand color and you're hunting through every file for bg-indigo-600. @apply solves this by letting you bundle a group of utilities into one reusable class, and @layer components tells Tailwind exactly where in the generated stylesheet that class belongs, so a one-off utility class used later in markup can still override it when needed.
This project builds a tiny component library — three button variants and two card variants — as real, reusable CSS classes (.btn-primary, .card, etc.), the same pattern used by every larger Tailwind-based design system.
@apply and @layer are compiled by Tailwind's real PostCSS build pipeline — they do not work through the zero-build Tailwind Play CDN script used in this course's other projects. Steps 1-4 below show the real, production @apply/@layer code you would write in an actual project (with the Tailwind CLI or a bundler running). The live preview at the bottom of this page substitutes hand-written plain CSS that reproduces the exact same visual result — see the note right before it for why.
Prerequisites
- Utility-first fundamentals — comfortable composing a design purely from utility classes first.
- The idea of a CSS build step — that
@tailwinddirectives and@applyare processed by PostCSS/the Tailwind CLI before a browser ever sees the output. - State variants (
hover:) and how they can still be bundled into an@applylist like any other utility. - Basic package.json / npm familiarity, since this project assumes a
tailwindcssinstall rather than the CDN script. - Grid Utilities, reused briefly in Step 4 to lay the demo cards out side by side.
Project Structure
Unlike the rest of this course's projects, this one is not a single HTML file — it is a small two-file setup, the same shape as a real Tailwind project: an input.css source file containing @tailwind directives plus your @layer components block, and an index.html (or component markup) that uses the resulting classes. A real project would also run npx tailwindcss -i ./input.css -o ./output.css --watch (or an equivalent bundler step) to compile input.css into plain CSS before the browser ever loads it.
Step 1: Set Up the @layer components Block
Every Tailwind project's source CSS starts with the three @tailwind directives, which is where Tailwind injects its own generated base styles, component classes, and utility classes at build time. @layer components is where custom, reusable classes like the buttons and cards below belong — it tells the build to place them AFTER Tailwind's base styles but BEFORE its utilities, so a plain utility class used later in markup can still win an specificity tie against one of these component classes.
/* input.css — processed by the Tailwind CLI/PostCSS build, NOT something a browser runs directly */
/* These three directives are placeholders Tailwind replaces at build time with its own generated CSS */@tailwind base;@tailwind components;@tailwind utilities;
/* @layer components: custom classes registered here sit between Tailwind's base and utility layers, so a plain utility class on an element (e.g. an extra p-2) can still override one of these component classes later — without @layer, that override order isn't guaranteed */@layer components { /* Step 2 and Step 3 fill this block in */}Step 2: Build Button Variants with @apply
@apply pastes in the real CSS that a list of utility classes already produces, at build time — so .btn becomes one ordinary CSS class, with no runtime utility scanning involved once it's compiled. Composing .btn inside .btn-primary's own @apply line is how a variant class picks up all of the shared button styling without repeating it.
@layer components { .btn { /* Shared shape/spacing/typography every button variant needs, in one place */ @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-semibold text-sm transition-colors duration-200 cursor-pointer; }
.btn-primary { /* Applying .btn INSIDE another @apply line is exactly how you build a variant without repeating every shared utility on every single variant class */ @apply btn bg-indigo-600 text-white hover:bg-indigo-700; }
.btn-secondary { @apply btn bg-slate-200 text-slate-900 hover:bg-slate-300; }
.btn-danger { @apply btn bg-rose-600 text-white hover:bg-rose-700; }}Step 3: Build Card Variants with @apply
The card variants follow the exact same "base class plus a composed variant" pattern as the buttons: .card-highlighted is defined as .card plus two overridden properties, rather than a separate, unrelated block of utilities.
@layer components { .card { @apply bg-white rounded-xl border border-slate-200 shadow-sm p-6; }
.card-title { @apply text-lg font-semibold text-slate-900 mb-2; }
.card-highlighted { /* A highlighted card IS a card, plus a bolder border and a slightly stronger shadow — @apply card here means every future change to .card's base styling also reaches this variant */ @apply card border-2 border-indigo-500 shadow-md; }}Step 4: Use the Component Classes in Markup
With the component classes compiled into real CSS, the markup that uses them looks almost like plain semantic HTML again — one class per button or card, instead of six or seven. Ordinary utility classes still layer on top of a component class without any conflict, as the last button below demonstrates.
<div class="p-8 bg-slate-50 space-y-8"> <!-- Each button below is ONE class — .btn-primary, .btn-secondary, .btn-danger — instead of repeating the same 6-7 shared utilities on every single button across the app --> <div class="flex gap-3"> <button class="btn-primary">Save Changes</button> <button class="btn-secondary">Cancel</button> <button class="btn-danger">Delete</button> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="card"> <h3 class="card-title">Standard Plan</h3> <p class="text-slate-600 text-sm">Everything you need to get started.</p> </div> <div class="card-highlighted"> <h3 class="card-title">Pro Plan</h3> <p class="text-slate-600 text-sm">Advanced features for growing teams.</p> </div> </div>
<!-- A plain utility class (mt-2) stacks on top of a component class with no conflict at all --> <button class="btn-primary mt-2">Utilities Still Work Alongside Component Classes</button></div>Complete Code
Here is the complete input.css source file from Steps 1-3, followed by the full markup from Step 4 that consumes it. In a real project, running the Tailwind build against this input.css produces a plain, ordinary output.css file containing real .btn-primary, .card, etc. rules — no @apply/@layer syntax survives into that compiled output, it is purely a build-time authoring convenience.
/* input.css */@tailwind base;@tailwind components;@tailwind utilities;
@layer components { .btn { @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-semibold text-sm transition-colors duration-200 cursor-pointer; }
.btn-primary { @apply btn bg-indigo-600 text-white hover:bg-indigo-700; }
.btn-secondary { @apply btn bg-slate-200 text-slate-900 hover:bg-slate-300; }
.btn-danger { @apply btn bg-rose-600 text-white hover:bg-rose-700; }
.card { @apply bg-white rounded-xl border border-slate-200 shadow-sm p-6; }
.card-title { @apply text-lg font-semibold text-slate-900 mb-2; }
.card-highlighted { @apply card border-2 border-indigo-500 shadow-md; }}<!-- index.html — links the COMPILED output.css, not input.css directly --><link rel="stylesheet" href="./output.css">
<div class="p-8 bg-slate-50 space-y-8"> <div class="flex gap-3"> <button class="btn-primary">Save Changes</button> <button class="btn-secondary">Cancel</button> <button class="btn-danger">Delete</button> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="card"> <h3 class="card-title">Standard Plan</h3> <p class="text-slate-600 text-sm">Everything you need to get started.</p> </div> <div class="card-highlighted"> <h3 class="card-title">Pro Plan</h3> <p class="text-slate-600 text-sm">Advanced features for growing teams.</p> </div> </div>
<button class="btn-primary mt-2">Utilities Still Work Alongside Component Classes</button></div>The Tailwind Play CDN script used for this course's live previews compiles utility classes on the fly in the browser, but it does not run a real PostCSS build — so @apply and @layer, which are compile-time-only directives, silently do nothing through it. To keep the demo below genuinely working, the preview's <style> block hand-writes the equivalent plain CSS that the real @apply rules from Steps 2-3 would have compiled to (identical class names, identical final property values), loaded alongside the Tailwind CDN script. The markup and visual result are otherwise exactly what the taught @apply/@layer code above produces in a real build.
Sample Run
Click Run to see what this code prints.
Extend This Project
- Add
.btn-sm/.btn-lgsize variants using the same "compose the base.btnclass" pattern from Step 2. - Add
dark:variants directly inside the@applylines (e.g.dark:bg-indigo-500) so the component library supports the Dark Mode Landing Page project's toggle. - Add a
.badgecomponent family (success/warning/danger) for small pill-shaped status labels. - Add visible
focus-visible:ring-2styles to.btnfor keyboard-navigation accessibility. - Package
input.css's compiled output as its own small npm package so the same component classes can be reused unchanged across multiple projects.
Summary
You built a small, real component library using @apply and @layer components — the exact mechanism behind every larger Tailwind-based design system. You also saw the one real limitation of that mechanism: it needs an actual PostCSS build, which is why this project's live preview substitutes hand-authored equivalent CSS rather than running the real @apply code. In your own projects, running the Tailwind CLI (or a bundler with the Tailwind plugin) is all that is needed to make the taught code in Steps 1-4 work exactly as shown.