Tailwind CSS

@apply & Component Extraction

20 min Lesson 23 of 35

@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:

Avoid @apply for:
  • 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;
  }
}
Layer Order:
  • 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:

The "Rule of Three":
  1. First use: Write utilities directly in HTML
  2. Second use: Copy and paste, make a mental note
  3. 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>
Framework Components vs @apply:
  • 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:

  1. Create a base .notification class with padding, rounded corners, border, and shadow
  2. Add variants: .notification-info, .notification-success, .notification-warning, .notification-error
  3. Create .notification-title and .notification-body subcomponents
  4. Add a .notification-dismissable variant with a close button
  5. Build HTML examples showcasing all variants
  6. Use the @layer directive to organize your code properly

Challenge Exercise

Advanced Task: Build a complete pricing card component system:

  1. Create base .pricing-card with header, body, and footer sections
  2. Add .pricing-card-featured variant with highlighted styling
  3. Create subcomponents: .pricing-header, .pricing-price, .pricing-features, .pricing-cta
  4. Add hover effects and transitions
  5. Make it responsive with different layouts for mobile/desktop
  6. Build a 3-tier pricing page (Basic, Pro, Enterprise) using your components
  7. Balance between @apply classes and inline utilities for flexibility
Pro Tip: Start with utility classes in your HTML. Only extract to @apply when you've copy-pasted the same pattern 3+ times. This prevents premature abstraction and keeps your CSS lean.

Summary

In this lesson, you've learned:

  • What @apply does 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 @layer directive 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.