LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
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.