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
For more Tailwind CSS topics, check out our Fundamentals, Customization, UI Components, Responsive Design, Dark Mode, Performance Optimization, Animations and Transitions, Framework Integration, Accessibility, and Plugins and Extensions pages.