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 `@tailwind` directives and `@apply` are 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 `@apply` list like any other utility.
- Basic package.json / npm familiarity, since this project assumes a `tailwindcss` install 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-lg` size variants using the same "compose the base `.btn` class" pattern from Step 2.
- Add `dark:` variants directly inside the `@apply` lines (e.g. `dark:bg-indigo-500`) so the component library supports the Dark Mode Landing Page project's toggle.
- Add a `.badge` component family (success/warning/danger) for small pill-shaped status labels.
- Add visible `focus-visible:ring-2` styles to `.btn` for 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.