Tailwind CSS Component Composition

Learn advanced strategies for composing and organizing Tailwind CSS components in large-scale applications.

The Component Composition Challenge

As projects grow, managing Tailwind utility classes becomes more complex. This page explores strategies for creating maintainable, reusable components.

Common Challenges

  • Class duplication: Repeating the same utility combinations across templates
  • Long class lists: Components with dozens of utility classes become hard to read
  • Maintainability: Changing a component's style requires updating multiple instances
  • Team collaboration: Ensuring consistent styling across multiple developers
  • Responsive complexity: Managing multiple breakpoint variants adds complexity

Extracting Patterns with @apply

The @apply directive allows you to extract common utility patterns into reusable CSS classes.

Basic @apply Usage

/* styles.css */
@tailwind base;
@tailwind components;

@layer components {
  .btn {
    @apply px-4 py-2 rounded font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2;
  }
  .btn-primary {
    @apply bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500;
  }
  .btn-secondary {
    @apply bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500;
  }
}

@tailwind utilities;

HTML usage:

<button class="btn btn-primary">
  Primary Button
</button>

<button class="btn btn-secondary">
  Secondary Button
</button>
When to use @apply:
  • For frequently repeated utility combinations
  • For complex components with many utilities
  • When you need to enforce consistency across a team

Advanced @apply with Variants

@layer components {
  .card {
    @apply bg-white rounded-lg shadow-md overflow-hidden;
  }
  
  .card-hover {
    @apply transition-all duration-300 hover:shadow-lg hover:-translate-y-1;
  }
  
  .input-field {
    @apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
    @apply focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500;
    @apply disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed;
  }
}

Component Extraction Strategies

Different approaches to extracting and organizing components with Tailwind CSS.

Template Partials

For template-based frameworks, extract common UI patterns into reusable partials or includes:

<!-- _button.html partial -->
<button class="px-4 py-2 rounded font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
  {{ text }}
</button>

<!-- Usage in main template -->
{% include "_button.html" with text="Click Me" %}

JavaScript Component Libraries

For JavaScript frameworks, create reusable component libraries:

// React Button component
function Button({ children, variant = 'primary', size = 'md', ...props }) {
  const baseClasses = "font-semibold rounded 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-2 py-1 text-sm",
    md: "px-4 py-2",
    lg: "px-6 py-3 text-lg",
  };
  
  const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`;
  
  return (
    <button className={classes} {...props}>
      {children}
    </button>
  );
}

// Usage
function App() {
  return (
    <div>
      <Button>Default Button</Button>
      <Button variant="secondary" size="sm">Small Secondary</Button>
      <Button variant="danger" size="lg">Large Danger</Button>
    </div>
  );
}

Hybrid Approach: Component Classes + Utilities

Combine base component classes with utility classes for customization:

/* styles.css */
@layer components {
  .card {
    @apply bg-white rounded-lg shadow-md overflow-hidden;
  }
}

/* HTML usage */
<div class="card p-6 lg:p-8 border border-gray-200">
  <h3 class="text-xl font-bold mb-4">Card Title</h3>
  <p>Card content with additional utility classes for specific styling.</p>
</div>

Managing Complex Class Lists

Techniques for organizing and managing long lists of utility classes.

Logical Grouping

Organize utility classes by their purpose:

<div class="
  /* Layout */
  flex flex-col md:flex-row items-center justify-between
  /* Spacing */
  p-4 md:p-6 gap-4
  /* Appearance */
  bg-white rounded-lg shadow-md
  /* Interactive */
  hover:shadow-lg focus-within:ring-2 focus-within:ring-blue-500
">
  <!-- Content here -->
</div>

Using Template Variables

For server-side templates, use variables to organize classes:

{% set buttonBase = "font-semibold rounded focus:outline-none focus:ring-2 focus:ring-offset-2" %}
{% set buttonPrimary = "bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500" %}
{% set buttonLarge = "px-6 py-3 text-lg" %}

<button class="{{ buttonBase }} {{ buttonPrimary }} {{ buttonLarge }}">
  Large Primary Button
</button>

Using JavaScript Libraries

Use libraries like clsx or classnames to manage complex class combinations:

// Using clsx
import clsx from 'clsx';

function Card({ title, children, isActive, isDisabled }) {
  const cardClasses = clsx(
    // Base classes
    'rounded-lg overflow-hidden',
    
    // Conditional classes
    {
      'bg-white shadow-md': !isActive && !isDisabled,
      'bg-blue-50 shadow-md ring-2 ring-blue-500': isActive,
      'bg-gray-100 opacity-60': isDisabled,
    }
  );
  
  return (
    <div className={cardClasses}>
      <div className="p-4">
        <h3 className="text-lg font-semibold">{title}</h3>
        <div className="mt-2">{children}</div>
      </div>
    </div>
  );
}

Multi-State Component Patterns

Strategies for managing components with multiple states and variants.

State-Based Component Classes

@layer components {
  /* Base alert */
  .alert {
    @apply p-4 rounded-md border-l-4;
  }
  
  /* Alert variants */
  .alert-info {
    @apply bg-blue-50 text-blue-800 border-blue-500;
  }
  
  .alert-success {
    @apply bg-green-50 text-green-800 border-green-500;
  }
  
  .alert-warning {
    @apply bg-yellow-50 text-yellow-800 border-yellow-500;
  }
  
  .alert-error {
    @apply bg-red-50 text-red-800 border-red-500;
  }
}

Usage:

<div class="alert alert-info">
  <p class="font-medium">Information</p>
  <p>This is an informational message.</p>
</div>

Data Attribute Selectors

Use data attributes to manage component states:

@layer components {
  .tab {
    @apply px-4 py-2 font-medium border-b-2 border-transparent;
  }
  
  .tab[data-state="active"] {
    @apply border-blue-500 text-blue-600;
  }
  
  .tab[data-state="inactive"] {
    @apply text-gray-500 hover:text-gray-700 hover:border-gray-300;
  }
}

Usage:

<div class="border-b border-gray-200">
  <nav class="flex">
    <a href="#" class="tab" data-state="active">Active Tab</a>
    <a href="#" class="tab" data-state="inactive">Inactive Tab</a>
    <a href="#" class="tab" data-state="inactive">Another Tab</a>
  </nav>
</div>

Responsive Component Patterns

Techniques for building components that adapt across different screen sizes.

Responsive Component Classes

@layer components {
  .card {
    @apply bg-white rounded-lg shadow-md overflow-hidden;
  }
  
  .card-horizontal {
    @apply md:flex;
  }
  
  .card-image {
    @apply w-full h-48 object-cover md:w-1/3 md:h-auto;
  }
  
  .card-content {
    @apply p-6 md:p-8 md:w-2/3;
  }
}

Usage:

<div class="card card-horizontal">
  <img src="image.jpg" alt="Card image" class="card-image">
  <div class="card-content">
    <h3 class="text-xl font-bold mb-2">Card Title</h3>
    <p class="text-gray-700">Card description text...</p>
  </div>
</div>

Responsive Utility Composition

Combine responsive utilities with component classes:

<!-- Navigation that changes from vertical to horizontal -->
<nav class="flex flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-4">
  <a href="#" class="nav-link">Home</a>
  <a href="#" class="nav-link">About</a>
  <a href="#" class="nav-link">Services</a>
  <a href="#" class="nav-link">Contact</a>
</nav>

Composition with JavaScript Frameworks

Framework-specific patterns for component composition.

React Composition Patterns

// Button.jsx - Composable button component
import React from 'react';
import clsx from 'clsx';

// Base button that accepts variants through props
function Button({ 
  children, 
  variant = 'primary', 
  size = 'md', 
  isFullWidth = false,
  className = '',
  ...props 
}) {
  const baseClasses = "font-semibold rounded 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",
    outline: "bg-transparent border border-current text-blue-500 hover:bg-blue-50",
  };
  
  const sizeClasses = {
    sm: "px-2 py-1 text-sm",
    md: "px-4 py-2",
    lg: "px-6 py-3 text-lg",
  };
  
  const widthClass = isFullWidth ? 'w-full' : '';
  
  const classes = clsx(
    baseClasses,
    variantClasses[variant],
    sizeClasses[size],
    widthClass,
    className
  );
  
  return (
    <button className={classes} {...props}>
      {children}
    </button>
  );
}

// IconButton component that builds on the base Button
function IconButton({ icon, children, ...props }) {
  return (
    <Button {...props} className="inline-flex items-center">
      {icon && <span className="mr-2">{icon}</span>}
      {children}
    </Button>
  );
}

// Usage
function App() {
  return (
    <div className="space-y-4">
      <Button variant="primary" size="lg">Primary Button</Button>
      
      <IconButton 
        icon={<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
          <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
          <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
        </svg>}
        variant="secondary"
      >
        View Details
      </IconButton>
      
      <Button variant="outline" isFullWidth>Full Width Outline Button</Button>
    </div>
  );
}

Vue Composition Patterns

<!-- BaseButton.vue -->
<template>
  <button 
    :class="[
      'font-semibold rounded focus:outline-none focus:ring-2 focus:ring-offset-2',
      variantClasses[variant],
      sizeClasses[size],
      { 'w-full': isFullWidth }
    ]"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'BaseButton',
  props: {
    variant: {
      type: String,
      default: 'primary',
      validator: value => ['primary', 'secondary', 'outline'].includes(value)
    },
    size: {
      type: String,
      default: 'md',
      validator: value => ['sm', 'md', 'lg'].includes(value)
    },
    isFullWidth: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    variantClasses() {
      return {
        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',
        outline: 'bg-transparent border border-current text-blue-500 hover:bg-blue-50'
      }
    },
    sizeClasses() {
      return {
        sm: 'px-2 py-1 text-sm',
        md: 'px-4 py-2',
        lg: 'px-6 py-3 text-lg'
      }
    }
  }
}
</script>

Best Practices for Component Composition

Guidelines for creating maintainable component systems with Tailwind CSS.

When to Use Each Approach

  • @apply: Use for simple, frequently repeated patterns that don't need dynamic props
  • Component libraries: Use for complex UI elements that need props and state management
  • Utility composition: Use for one-off or highly customized instances

Naming Conventions

  • Use consistent, descriptive names for component classes
  • Follow BEM-like naming for component variants (e.g., btn-primary, card-horizontal)
  • Use namespaces to avoid conflicts (e.g., ui-btn, ui-card)

Documentation

  • Document your component system with examples
  • Create a style guide or component library
  • Include responsive behavior and state variations in documentation

Consistency Tips

  • Establish team conventions for component composition
  • Create a shared library of common components
  • Use linting tools to enforce consistent usage
  • Review component usage regularly to identify patterns that should be extracted