← Back to Tailwind CSS Mastery
Intermediate14 min read

Component Development

Build reusable components with Tailwind — extraction patterns, @apply, class merging utilities, and component library architecture.

Component Extraction

When the same utility combination appears three or more times, extract it into a component. In React, create a Button component that accepts variant props and composes Tailwind classes.

Keep extracted components flexible with props for size, variant, and className overrides. Avoid over-abstracting — not every utility combination needs a component.

  • Use cva (class-variance-authority) for type-safe variant APIs
  • Accept className prop for consumer overrides
  • Document component variants in Storybook
function Button({ variant = 'primary', children, className }) {
  const variants = {
    primary: 'bg-blue-600 hover:bg-blue-700 text-white',
    ghost: 'bg-transparent hover:bg-gray-100 text-gray-700',
  };
  return (
    <button className={cn('px-4 py-2 rounded-lg font-semibold', variants[variant], className)}>
      {children}
    </button>
  );
}

The @apply Directive

Extract utility combinations into named CSS classes with @apply. Use sparingly — overusing @apply defeats the purpose of utility-first CSS and reintroduces the specificity problems Tailwind solves.

Reserve @apply for base styles, third-party component overrides, and legacy CSS integration. Prefer component extraction in JavaScript for React/Vue projects.

@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-blue-600 text-white rounded-lg
           font-semibold hover:bg-blue-700 transition-colors;
  }
}

Class Merging with cn/clsx

The cn utility (clsx + tailwind-merge) resolves conflicting Tailwind classes intelligently. cn("px-4", "px-6") produces "px-6", not both. This is essential for components that accept className overrides.

Install clsx and tailwind-merge, then compose them into a cn helper used project-wide.

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// Usage: later classes win
cn('px-4 py-2', 'px-6') // → 'py-2 px-6'

Headless Component Patterns

Combine Tailwind with headless UI libraries (Radix, Headless UI) for accessible, unstyled behavior. Tailwind handles appearance; headless libraries handle keyboard navigation, ARIA, and focus management.

This separation produces accessible components without sacrificing design flexibility. shadcn/ui popularized this pattern with copy-paste components.

import * as Dialog from '@radix-ui/react-dialog';

<Dialog.Root>
  <Dialog.Trigger className="btn-primary">Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2
      -translate-y-1/2 bg-white rounded-xl p-6 shadow-xl">
      {children}
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Component Library Architecture

Organize components in a ui/ directory with one file per component. Export from an index barrel file. Share tokens through tailwind.config.js theme extensions.

Document components with Storybook or Ladle. Each story demonstrates variants, sizes, and states. This living documentation keeps the team aligned on available components.

components/
├── ui/
│   ├── button.tsx
│   ├── card.tsx
│   ├── input.tsx
│   └── index.ts
└── lib/
    └── cn.ts

Get In Touch


Ready to discuss your next project? Drop me a message.