On this page
Reusability and Components
Reusability and Components
One of the most common questions Tailwind developers ask is: "How do I avoid repeating the same 20 classes on every button?" The answer is not to abandon utility-first CSS — it is to use the right reuse strategies for each context.
The three reuse strategies
1. HTML/template components (preferred in JS frameworks)
The most idiomatic way to avoid repetition in React, Vue, Angular, or Svelte is to extract the repeated HTML into a framework component:
// React: ButtonPrimary.tsx
interface ButtonProps {
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
}
export function ButtonPrimary({ children, disabled, onClick }: ButtonProps) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
className="inline-flex items-center justify-center gap-2 font-semibold rounded-xl px-5 py-2.5 text-sm bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white transition-all duration-150 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{children}
</button>
);
}The class string lives in exactly one place — the component file. Every use site calls <ButtonPrimary> with no class boilerplate.
2. CSS @apply (ideal for HTML-only projects)
When you are in a plain HTML/CSS environment without a component system, @apply lets you define reusable utility groups:
@layer components {
.btn-primary {
@apply inline-flex items-center justify-center font-semibold rounded-xl
px-5 py-2.5 text-sm bg-indigo-600 hover:bg-indigo-700
text-white transition-all duration-150
focus-visible:ring-2 focus-visible:ring-indigo-500;
}
}Usage is then <button class="btn-primary">.
3. Utility multi-class with editor tooling
For small teams working in HTML, using the Tailwind IntelliSense extension with multi-cursor editing and snippets can be enough. Define a VS Code snippet for btn-primary that expands to the full class list.
The @apply directive
@apply composes Tailwind utilities into a custom CSS class. It is part of the components layer and should only be used there.
@import "tailwindcss";
@layer components {
/* Simple composition */
.prose-link {
@apply text-indigo-600 underline decoration-indigo-300 hover:text-indigo-800
hover:decoration-indigo-500 transition-colors;
}
/* Using responsive prefixes in @apply */
.page-container {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
}
/* Using dark mode in @apply */
.surface {
@apply bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100;
}
/* Using state variants in @apply */
.nav-link {
@apply text-sm font-medium text-gray-600 hover:text-indigo-600
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500
rounded transition-colors px-1;
}
}The @layer directive
Tailwind v4 uses three CSS cascade layers:
| Layer | Purpose | Specificity |
|---|---|---|
base |
CSS reset, element-level defaults | Lowest |
components |
Reusable class abstractions | Medium |
utilities |
Single-purpose utility classes | Highest |
The higher specificity of utilities means you can always override a component class with a utility class in HTML:
<!-- The btn class sets px-5, but px-8 in HTML overrides it -->
<button class="btn-primary px-8">Wide button</button>Adding custom styles to each layer:
@import "tailwindcss";
/* Base: global defaults that apply to HTML elements */
@layer base {
* {
@apply border-box;
}
body {
@apply bg-white text-gray-900 antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold tracking-tight text-balance;
}
a {
@apply text-indigo-600 hover:text-indigo-800 transition-colors;
}
}
/* Components: reusable multi-utility classes */
@layer components {
.btn { ... }
.card { ... }
.form-input { ... }
}
/* Utilities: custom single-purpose utilities */
@layer utilities {
.text-shadow-sm {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.scrollbar-hide {
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}Building a complete design system
A production-ready component set typically includes:
Button system
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2 font-semibold rounded-xl
px-5 py-2.5 text-sm transition-all duration-150
focus-visible:ring-2 focus-visible:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none;
}
.btn-sm { @apply btn px-3 py-1.5 text-xs rounded-lg; }
.btn-lg { @apply btn px-7 py-3.5 text-base rounded-2xl; }
.btn-primary { @apply btn bg-indigo-600 hover:bg-indigo-700 text-white focus-visible:ring-indigo-500; }
.btn-secondary { @apply btn bg-white hover:bg-gray-50 border border-gray-200 text-gray-700 focus-visible:ring-gray-400; }
.btn-danger { @apply btn bg-red-600 hover:bg-red-700 text-white focus-visible:ring-red-500; }
}Alert/banner system
@layer components {
.alert {
@apply flex items-start gap-3 px-4 py-3 rounded-xl text-sm font-medium;
}
.alert-info { @apply alert bg-blue-50 text-blue-800 border border-blue-200; }
.alert-success { @apply alert bg-green-50 text-green-800 border border-green-200; }
.alert-warning { @apply alert bg-amber-50 text-amber-800 border border-amber-200; }
.alert-error { @apply alert bg-red-50 text-red-800 border border-red-200; }
}Usage in HTML:
<div class="alert-success" role="alert">
<svg class="w-5 h-5 shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Profile saved successfully.
</div>Framework integration patterns
Angular
In Angular, reusability happens at the component level. Define reusable components and keep the class strings in the component TypeScript file:
// button.component.ts
import { Component, input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button
[type]="type()"
[disabled]="disabled()"
[class]="classes()"
>
<ng-content />
</button>
`
})
export class ButtonComponent {
variant = input<'primary' | 'secondary' | 'ghost'>('primary');
size = input<'sm' | 'md' | 'lg'>('md');
disabled = input(false);
type = input<'button' | 'submit' | 'reset'>('button');
protected classes = computed(() => {
const base = 'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-150 focus-visible:ring-2 focus-visible:ring-offset-2';
const variants = {
primary: 'bg-indigo-600 hover:bg-indigo-700 text-white focus-visible:ring-indigo-500',
secondary: 'bg-white hover:bg-gray-50 border border-gray-200 text-gray-700 focus-visible:ring-gray-400',
ghost: 'bg-transparent hover:bg-gray-100 text-gray-600 focus-visible:ring-gray-400'
};
const sizes = { sm: 'px-3 py-1.5 text-xs', md: 'px-5 py-2.5 text-sm', lg: 'px-7 py-3.5 text-base' };
return `${base} ${variants[this.variant()]} ${sizes[this.size()]}`;
});
}Practice
- Build a complete badge system in
@layer componentswith five variants (default, primary, success, warning, error) using@apply. Test all of them in an HTML file. - Create a
page-sectionutility class that applies consistent max-width, padding, and centering. Use it on three different sections. - In a framework of your choice, extract a
Cardcomponent that accepts atitle,description, and optionalbadgeslot. Pass Tailwind classes as props for different color themes.
@import "tailwindcss";
@theme {
--color-brand: #6366f1;
--color-brand-dark: #4f46e5;
}
/* ============================================
Component layer: reusable CSS classes
that compose Tailwind utilities with @apply
============================================ */
@layer components {
/* Button variants */
.btn {
@apply inline-flex items-center justify-center gap-2
font-semibold rounded-xl px-5 py-2.5 text-sm
transition-all duration-150 focus-visible:ring-2
focus-visible:ring-offset-2 disabled:opacity-50
disabled:cursor-not-allowed;
}
.btn-primary {
@apply btn bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800
text-white focus-visible:ring-indigo-500;
}
.btn-secondary {
@apply btn bg-white hover:bg-gray-50 active:bg-gray-100
text-gray-700 border border-gray-200 hover:border-gray-300
focus-visible:ring-gray-400;
}
.btn-ghost {
@apply btn bg-transparent hover:bg-gray-100 active:bg-gray-200
text-gray-600 hover:text-gray-900 focus-visible:ring-gray-400;
}
/* Card */
.card {
@apply bg-white rounded-2xl border border-gray-100
shadow-sm overflow-hidden;
}
.card-header {
@apply px-6 py-4 border-b border-gray-100;
}
.card-body {
@apply px-6 py-5;
}
/* Input */
.form-input {
@apply w-full bg-white border border-gray-200 rounded-lg
px-3 py-2.5 text-sm text-gray-900
placeholder-gray-400 outline-none
transition-all duration-150
focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20
disabled:bg-gray-50 disabled:text-gray-400
disabled:cursor-not-allowed;
}
/* Badge variants */
.badge {
@apply inline-flex items-center text-xs font-semibold
px-2.5 py-0.5 rounded-full;
}
.badge-indigo {
@apply badge bg-indigo-100 text-indigo-700;
}
.badge-green {
@apply badge bg-green-100 text-green-700;
}
.badge-red {
@apply badge bg-red-100 text-red-700;
}
}
Sign in to track your progress