@apply & Component Extraction
@apply & Component Extraction
While Tailwind encourages a utility-first approach with classes in your HTML, there are times when you need to extract repeated patterns into reusable components or create custom utility classes. The @apply directive is your tool for this—but it must be used wisely.
In this lesson, we'll explore when and how to use @apply, understand the philosophy behind component extraction, and learn best practices for balancing utility-first CSS with component-based patterns.
What is @apply?
The @apply directive allows you to compose Tailwind utility classes into custom CSS classes. It extracts utility classes from your HTML and moves them into your CSS:
Basic @apply Example
/* In your CSS file */
.btn-primary {
@apply px-6 py-3 bg-blue-500 text-white rounded-lg;
@apply hover:bg-blue-600 focus:outline-none focus:ring-2;
@apply focus:ring-blue-500 focus:ring-offset-2;
@apply transition-colors duration-200;
}
Now in your HTML, instead of this:
Before @apply (Verbose HTML)
<button class="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2 transition-colors duration-200">
Click Me
</button>
You can write this:
After @apply (Clean HTML)
<button class="btn-primary">
Click Me
</button>
When to Use @apply
Before reaching for @apply, ask yourself: "Am I using this exact pattern in multiple places?" Here are good use cases:
1. Repeated Component Patterns
Button System with @apply
/* Base button styles */
.btn {
@apply inline-flex items-center justify-center;
@apply px-4 py-2 rounded-md font-medium;
@apply transition-colors duration-200;
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
}
/* Button variants */
.btn-primary {
@apply btn bg-blue-500 text-white;
@apply hover:bg-blue-600 focus:ring-blue-500;
}
.btn-secondary {
@apply btn bg-gray-200 text-gray-800;
@apply hover:bg-gray-300 focus:ring-gray-500;
}
.btn-danger {
@apply btn bg-red-500 text-white;
@apply hover:bg-red-600 focus:ring-red-500;
}
/* Button sizes */
.btn-sm {
@apply px-3 py-1.5 text-sm;
}
.btn-lg {
@apply px-6 py-3 text-lg;
}
Using Button Classes
<button class="btn btn-primary">Primary Button</button>
<button class="btn btn-secondary btn-sm">Small Secondary</button>
<button class="btn btn-danger btn-lg">Large Danger</button>
2. Form Elements
Form Component Classes
/* Input field base styles */
.input {
@apply w-full px-4 py-2 border border-gray-300 rounded-md;
@apply focus:outline-none focus:ring-2 focus:ring-blue-500;
@apply focus:border-transparent transition-all duration-200;
}
.input-error {
@apply input border-red-500 focus:ring-red-500;
}
/* Label styles */
.label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.label-required::after {
content: " *";
@apply text-red-500;
}
/* Form group */
.form-group {
@apply mb-4;
}
/* Error message */
.error-message {
@apply mt-1 text-sm text-red-600;
}
Using Form Classes
<div class="form-group">
<label class="label label-required">Email</label>
<input type="email" class="input" placeholder="you@example.com">
</div>
<div class="form-group">
<label class="label">Password</label>
<input type="password" class="input-error">
<p class="error-message">Password must be at least 8 characters</p>
</div>
3. Card Components
Card System with @apply
/* Base card */
.card {
@apply bg-white rounded-lg shadow-md overflow-hidden;
@apply border border-gray-200;
}
/* Card sections */
.card-header {
@apply px-6 py-4 border-b border-gray-200;
@apply bg-gray-50;
}
.card-body {
@apply px-6 py-4;
}
.card-footer {
@apply px-6 py-4 border-t border-gray-200;
@apply bg-gray-50;
}
/* Card title */
.card-title {
@apply text-xl font-semibold text-gray-900;
}
/* Card variants */
.card-hover {
@apply card transition-all duration-300;
@apply hover:shadow-lg hover:-translate-y-1;
}
.card-interactive {
@apply card cursor-pointer;
@apply hover:border-blue-500 hover:shadow-lg;
}
When NOT to Use @apply
Understanding when to avoid @apply is just as important as knowing when to use it:
- One-off styles: If you only use a pattern once, keep utilities in HTML
- Simple combinations: 2-3 utilities don't justify extraction
- Page-specific styles: Unique layouts should stay in HTML
- Framework components: React, Vue, etc. handle composition better
- Early optimization: Wait until patterns repeat 3+ times
Bad Use of @apply (Don't Do This)
/* DON'T: Extracting everything */
.my-div {
@apply w-full h-full bg-white p-4 m-2 rounded shadow;
}
/* DON'T: One-off styles */
.about-page-hero {
@apply h-screen flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600;
}
/* DON'T: Simple combinations */
.flex-center {
@apply flex items-center justify-center;
}
/* This is better kept in HTML or as a component */
The @layer Directive
When using @apply, organize your custom styles with the @layer directive. This tells Tailwind where your custom CSS belongs in the cascade:
Using @layer for Organization
/* In your main CSS file */
/* Base layer - HTML element defaults */
@layer base {
h1 {
@apply text-4xl font-bold text-gray-900;
}
h2 {
@apply text-3xl font-semibold text-gray-800;
}
a {
@apply text-blue-600 hover:text-blue-800 underline;
}
body {
@apply font-sans text-gray-900 antialiased;
}
}
/* Components layer - reusable components */
@layer components {
.btn {
@apply px-4 py-2 rounded-md font-medium;
@apply transition-colors duration-200;
}
.btn-primary {
@apply btn bg-blue-500 text-white;
@apply hover:bg-blue-600;
}
.card {
@apply bg-white rounded-lg shadow-md p-6;
}
.input {
@apply w-full px-4 py-2 border rounded-md;
@apply focus:outline-none focus:ring-2;
}
}
/* Utilities layer - custom utility classes */
@layer utilities {
.text-shadow {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.bg-grid {
background-image: repeating-linear-gradient(
0deg, #e5e7eb 0px, #e5e7eb 1px,
transparent 1px, transparent 20px
);
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
- base: Loaded first, for HTML element defaults
- components: Loaded second, for component classes
- utilities: Loaded last, highest specificity
Utilities can override components, and components can override base styles.
Extracting Complex Patterns
Sometimes you need to combine @apply with regular CSS for complex patterns:
Complex Component with Mixed Styles
@layer components {
/* Dropdown component */
.dropdown {
@apply relative inline-block;
}
.dropdown-trigger {
@apply px-4 py-2 bg-white border border-gray-300 rounded-md;
@apply hover:bg-gray-50 focus:outline-none;
cursor: pointer;
}
.dropdown-menu {
@apply absolute left-0 mt-2 w-56 rounded-md shadow-lg;
@apply bg-white ring-1 ring-black ring-opacity-5;
@apply opacity-0 invisible transition-all duration-200;
@apply transform origin-top-left scale-95;
z-index: 1000;
}
.dropdown.open .dropdown-menu {
@apply opacity-100 visible scale-100;
}
.dropdown-item {
@apply block px-4 py-2 text-sm text-gray-700;
@apply hover:bg-gray-100 hover:text-gray-900;
@apply transition-colors duration-150;
}
.dropdown-divider {
@apply my-1 border-t border-gray-200;
}
/* Tooltip component */
.tooltip {
@apply relative inline-block;
}
.tooltip-text {
@apply invisible absolute z-10 w-32 px-3 py-2;
@apply bg-gray-900 text-white text-sm text-center rounded-md;
@apply -top-12 left-1/2 -translate-x-1/2;
@apply opacity-0 transition-opacity duration-300;
}
.tooltip-text::after {
content: "";
@apply absolute top-full left-1/2 -translate-x-1/2;
border-width: 5px;
border-style: solid;
border-color: #1f2937 transparent transparent transparent;
}
.tooltip:hover .tooltip-text {
@apply visible opacity-100;
}
}
Component Extraction Best Practices
Follow these principles when deciding whether to extract components:
- First use: Write utilities directly in HTML
- Second use: Copy and paste, make a mental note
- Third use: Consider extracting to @apply class
Don't abstract prematurely—wait for clear patterns to emerge.
Composable Over Monolithic
Good: Composable Button System
/* Small, composable classes */
.btn {
@apply px-4 py-2 rounded-md font-medium;
}
.btn-solid {
@apply btn;
}
.btn-outline {
@apply btn border-2 bg-transparent;
}
/* Colors as separate classes */
.btn-blue {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.btn-red {
@apply bg-red-500 text-white hover:bg-red-600;
}
/* Usage: mix and match */
<button class="btn btn-solid btn-blue">Solid Blue</button>
<button class="btn btn-outline btn-red">Outline Red</button>
Bad: Monolithic Button Classes
/* DON'T: Too specific, not composable */
.btn-solid-blue {
@apply px-4 py-2 rounded-md font-medium;
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.btn-solid-red {
@apply px-4 py-2 rounded-md font-medium;
@apply bg-red-500 text-white hover:bg-red-600;
}
.btn-outline-blue {
@apply px-4 py-2 rounded-md font-medium;
@apply border-2 border-blue-500 text-blue-500 bg-transparent;
}
/* Too many specific classes, lots of duplication */
Balancing Utility-First with Components
The key is finding the right balance between utility classes and component classes:
Balanced Approach Example
/* Extract common patterns */
@layer components {
.card {
@apply bg-white rounded-lg shadow-md p-6;
}
.btn {
@apply px-4 py-2 rounded-md font-medium;
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply transition-colors duration-200;
}
}
/* HTML: Component classes + utility modifiers */
<div class="card border border-gray-200">
<h2 class="text-2xl font-bold mb-4">Card Title</h2>
<p class="text-gray-600 mb-6">Card content goes here</p>
<button class="btn bg-blue-500 text-white hover:bg-blue-600">
Action
</button>
</div>
Framework Component Patterns
In JavaScript frameworks, consider component-based extraction instead of CSS classes:
React Component (Often Better Than @apply)
// Button.jsx
function Button({ variant = 'primary', size = 'md', children }) {
const baseClasses = 'inline-flex items-center justify-center ' +
'rounded-md font-medium transition-colors ' +
'focus:outline-none focus:ring-2 focus:ring-offset-2';
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500'
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
>
{children}
</button>
);
}
// Usage
<Button variant="primary" size="lg">Click Me</Button>
<Button variant="danger">Delete</Button>
- Use @apply: For static HTML sites, templates, or truly global patterns
- Use components: In React/Vue/Svelte for better composition and props
Real-World Component Library
Here's a practical component library using @apply:
Complete Component System
@layer components {
/* Buttons */
.btn {
@apply inline-flex items-center justify-center;
@apply px-4 py-2 border border-transparent rounded-md;
@apply font-medium text-sm transition-all duration-200;
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary { @apply btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500; }
.btn-secondary { @apply btn bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500; }
.btn-success { @apply btn bg-green-600 text-white hover:bg-green-700 focus:ring-green-500; }
.btn-danger { @apply btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500; }
/* Badges */
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full;
@apply text-xs font-medium;
}
.badge-primary { @apply badge bg-blue-100 text-blue-800; }
.badge-success { @apply badge bg-green-100 text-green-800; }
.badge-warning { @apply badge bg-yellow-100 text-yellow-800; }
.badge-danger { @apply badge bg-red-100 text-red-800; }
/* Alerts */
.alert {
@apply p-4 rounded-md border;
}
.alert-info { @apply alert bg-blue-50 border-blue-200 text-blue-800; }
.alert-success { @apply alert bg-green-50 border-green-200 text-green-800; }
.alert-warning { @apply alert bg-yellow-50 border-yellow-200 text-yellow-800; }
.alert-error { @apply alert bg-red-50 border-red-200 text-red-800; }
/* Form Elements */
.form-input {
@apply w-full px-3 py-2 border border-gray-300 rounded-md;
@apply placeholder-gray-400 focus:outline-none;
@apply focus:ring-2 focus:ring-blue-500 focus:border-transparent;
@apply transition-all duration-200;
}
.form-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.form-error {
@apply text-sm text-red-600 mt-1;
}
/* Links */
.link {
@apply text-blue-600 hover:text-blue-800 underline;
@apply transition-colors duration-200;
}
.link-muted {
@apply text-gray-600 hover:text-gray-900 no-underline;
}
}
Practice Exercise
Task: Create a notification component system using @apply:
- Create a base
.notificationclass with padding, rounded corners, border, and shadow - Add variants:
.notification-info,.notification-success,.notification-warning,.notification-error - Create
.notification-titleand.notification-bodysubcomponents - Add a
.notification-dismissablevariant with a close button - Build HTML examples showcasing all variants
- Use the @layer directive to organize your code properly
Challenge Exercise
Advanced Task: Build a complete pricing card component system:
- Create base
.pricing-cardwith header, body, and footer sections - Add
.pricing-card-featuredvariant with highlighted styling - Create subcomponents:
.pricing-header,.pricing-price,.pricing-features,.pricing-cta - Add hover effects and transitions
- Make it responsive with different layouts for mobile/desktop
- Build a 3-tier pricing page (Basic, Pro, Enterprise) using your components
- Balance between @apply classes and inline utilities for flexibility
Summary
In this lesson, you've learned:
- What
@applydoes and how to use it effectively - When to use @apply (repeated patterns, component systems)
- When NOT to use @apply (one-offs, simple combos, framework components)
- The
@layerdirective for organizing custom styles - Best practices for component extraction (Rule of Three)
- Balancing utility-first approach with component classes
- Composable vs monolithic component design
- Framework-specific patterns (React/Vue components vs CSS classes)
The key to mastering @apply is restraint—use it when it genuinely improves maintainability, not just because you can. In the next lesson, we'll explore dark mode implementation in Tailwind.