LearnAI ToolsCareerPractice BuildsPlayContact
Tailwind CSSIntermediate~1.5 hours

Custom Component Library

Build a small reusable button/card system using @apply and @layer.

@apply@layerTheming

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.

A Build Step Is Required for This One

@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>
About the Live Preview Below

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.

Live Preview

Sample Run

What You'll See

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.