· 6 min read

Creating a Component Library with Tailwind CSS: A Practical Guide

Step-by-step tutorial for building a reusable, accessible component library using Tailwind CSS. Includes Tailwind config, @apply components, React component patterns, theming, Storybook, testing, and publishing tips with full code examples.

Introduction

Tailwind CSS is a utility-first framework that makes building consistent UI fast and predictable. But for larger products you want reusable, well-documented components: buttons, cards, inputs, modals, etc. This guide shows a practical workflow for creating a component library with Tailwind CSS - from planning and config to implementation, theming, accessibility, Storybook integration, and publishing.

We’ll cover both plain CSS-driven components (using Tailwind’s @apply) and framework components (React examples). Links to official docs and tools are included so you can deep-dive where you like.

References

  1. Plan your design tokens and scale

Before you build components, plan the tokens and scales you’ll rely on. Tailwind already provides scales (spacing, font-size, colors), but decide which keys you’ll use, which color names, and any additional tokens (like roundedness, elevation) you want exposed.

Example decisions:

  • Spacing: use Tailwind’s 0-96 scale
  • Colors: semantic names - primary, neutral-50..900, success, danger, warning
  • Radii: sm, md, lg
  • Elevation: shadow-none, shadow-sm, shadow-md

Add these to tailwind.config.js so they become authoritative.

Example tailwind.config.js

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx,html}'],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9', // primary-500
        },
        neutral: {
          50: '#f9fafb',
          100: '#f3f4f6',
          900: '#111827',
        },
      },
      borderRadius: {
        md: '0.5rem',
      },
      boxShadow: {
        sm: '0 1px 2px rgba(0,0,0,0.05)',
        md: '0 4px 8px rgba(0,0,0,0.08)',
      },
    },
  },
  plugins: [],
};
  1. Set up base styles and component layer using @apply

Tailwind’s @apply allows you to compose utilities into semantic classes. That’s a great way to create a CSS-driven component library (framework-agnostic) that still benefits from Tailwind’s utility engine.

Create a CSS file (src/styles/components.css) and use layers to expose components.

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

@layer components {
  .btn {
    @apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium select-none;
  }

  .btn-primary {
    @apply btn bg-primary-500 text-white hover:bg-primary-600 shadow-sm;
  }

  .btn-ghost {
    @apply btn bg-transparent text-primary-500 hover:bg-primary-50;
  }

  .card {
    @apply bg-white rounded-md shadow-md p-4;
  }

  .input {
    @apply block w-full px-3 py-2 rounded-md border border-neutral-200 focus:(outline-none ring-2 ring-primary-200);
  }
}

Why @layer components? It makes sure your classes are part of the compiled CSS in the components layer and play nicely with Tailwind’s ordering.

  1. Build accessible React components

While CSS classes are reusable across frameworks, many teams prefer component wrappers for API surface, prop handling, and accessibility logic. Below is a typical React Button component using Tailwind classes with a variant system and proper ref forwarding.

Install helpers:

npm install clsx
# or
yarn add clsx

Button.tsx (React + TypeScript)

import React from 'react';
import clsx from 'clsx';

type Variant = 'primary' | 'ghost';

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: Variant;
  size?: 'sm' | 'md' | 'lg';
};

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', className, children, ...rest }, ref) => {
    const base =
      'inline-flex items-center justify-center font-medium select-none rounded-md';

    const sizes = {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-3 text-lg',
    };

    const variants = {
      primary: 'bg-primary-500 text-white hover:bg-primary-600',
      ghost: 'bg-transparent text-primary-500 hover:bg-primary-50',
    };

    return (
      <button
        {...rest}
        ref={ref}
        className={clsx(base, sizes[size], variants[variant], className)}
      >
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';

Notes:

  • Use clsx (or classnames) to compose conditional classes cleanly.
  • Forwarding ref helps consumers integrate with forms and focus management.
  • Keep prop API minimal and predictable - variant + size + className.
  1. Compose complex components (Card, Input, Modal)

Card component pattern (composition with children):

export const Card: React.FC<{ className?: string }> = ({
  className,
  children,
}) => {
  return (
    <div className={clsx('bg-white rounded-md shadow-md p-4', className)}>
      {children}
    </div>
  );
};

Input component with error state:

type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  error?: string;
};

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ error, className, ...rest }, ref) => {
    return (
      <div>
        <input
          ref={ref}
          className={clsx(
            'block w-full px-3 py-2 rounded-md border',
            error ? 'border-red-500' : 'border-neutral-200',
            className
          )}
          {...rest}
        />
        {error && <p className="mt-1 text-sm text-red-600">{error}</p>}
      </div>
    );
  }
);

Modal: prefer to use a headless primitive (Headless UI or Radix) for accessibility. Example using Headless UI’s Dialog (see docs): https://headlessui.dev/

  1. Theming and runtime color switching

For dynamic themes (light/dark or brand switching), combine Tailwind and CSS variables. Define CSS variables in :root and map them in tailwind config with the colors function or keep Tailwind colors and use utility classes for dark mode.

Simple approach: use Tailwind’s dark mode class-based strategy in config:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
};

For runtime brand colors, set CSS variables on the root and reference them in your components via custom classes using var(—color-primary).

  1. Documentation with Storybook

Storybook provides an isolated playground for components, documentation, and visual tests.

Install and init Storybook:

npx sb init

Add a preview wrapper to include your Tailwind CSS in Storybook (preview.js). Document variants for each component and include args for interactive controls.

Useful Storybook addons:

  • Docs (built-in)
  • Controls
  • Accessibility (a11y)

Official Storybook: https://storybook.js.org/

  1. Visual regression and accessibility testing
  • Visual regression: Chromatic, Percy, or Playwright/VRT setups.
  • Accessibility: Use axe-core or the Storybook a11y addon to catch ARIA issues early.
  1. Packaging and distribution

If you’re publishing a framework-agnostic CSS bundle, compile your Tailwind CSS and publish the CSS file. If you’re publishing a component library (React), bundle with a proper build setup (Vite, Rollup, or tsup) and export both ESM and CJS. Consider a monorepo if you need multiple packages (components + CSS).

Quick publish checklist:

  • Build: transpile TypeScript and generate ESM/CJS
  • CSS: generate Tailwind CSS output for consumers or include source files + tailwind config guidance
  • Types: include TypeScript declaration files (.d.ts)
  • README: usage, import examples, theming

Example package scripts (package.json):

{
  "scripts": {
    "build": "tsc && rollup -c",
    "build:css": "NODE_ENV=production npx tailwindcss -i ./src/styles/components.css -o ./dist/styles.css"
  }
}
  1. Best practices
  • Think API first: design the component props before implementation.
  • Keep components small and composable: prefer composition over large monolithic components.
  • Accessibility by default: keyboard focus, ARIA, and proper roles.
  • Minimal runtime logic: leverage Tailwind for styling and keep JS for behavior and state.
  • Theming: prefer semantic color names (primary, surface, text) rather than raw colors.
  • Documentation: story examples, visual tests, and a changelog.
  • Performance: purge unused classes (Tailwind’s content option) and ship only what you need.
  • Tests: unit tests for behavior, visual tests for regressions, and a11y audits.
  1. Real-world examples and patterns
  • Variant maps: maintain your variant-to-class mapping in a single place (like the Button example above) to avoid duplication.
  • Utility bridging: when a user passes className, always merge last so they can opt out or extend styles.
  • Headless primitives: rely on Headless UI or Radix for complex ARIA patterns (menus, popovers, dialogs).

Small example: Variant map with typed keys (TypeScript)

const buttonStyles = {
  base: 'inline-flex items-center justify-center rounded-md font-medium',
  variants: {
    primary: 'bg-primary-500 text-white hover:bg-primary-600',
    ghost: 'bg-transparent text-primary-500 hover:bg-primary-50',
  },
} as const;

type ButtonVariant = keyof (typeof buttonStyles)['variants'];
  1. Example repository structure

Suggested layout for a React + CSS component library:

  • package.json
  • src/
    • components/
      • Button/
        • Button.tsx
        • Button.stories.tsx
      • Input/
    • styles/
      • components.css
    • index.ts (exports)
  • dist/ (built output)
  • storybook/
  1. Final notes

Creating a component library with Tailwind CSS is about balancing the utility-first approach with semantic, reusable APIs. Use Tailwind’s config to lock down your design language, @apply for framework-agnostic CSS components, and wrapper components in your framework of choice for accessibility and runtime behavior.

Useful resources

If you follow the patterns above you’ll end up with a maintainable, well-documented component library that scales across teams and products.

Back to Blog

Related Posts

View All Posts »

The Future of Tailwind CSS: Predictions for 2024 and Beyond

A forward-looking analysis of how trends in web design and browser capabilities - from container queries and cascade layers to AI-driven tooling and design tokens - will shape Tailwind CSS and the wider utility-first movement in 2024 and beyond.

Styling Tricks in React: Making Your UI Stand Out

A practical, in-depth guide to styling React apps: choosing between CSS-in-JS, utility-first, and modular CSS; responsive and modern layout techniques; dynamic, state-driven styling; animations; accessibility and performance tips to make your UI pop.