Tailwind Component Patterns
The most common criticism of Tailwind is "long class lists in the markup". In practice, the answer isn't hiding them with @apply everywhere. It's the same answer as for any repetition in code: extract components. A <Button variant="primary" size="sm"> encapsulates its utilities once, and the rest of the app uses the component.
Mature Tailwind codebases share a toolkit: component extraction in your framework, typed variant APIs (class-variance-authority or tailwind-variants), a cn() helper combining clsx and tailwind-merge so consumers can override classes safely, automatic class sorting with Prettier, and headless UI libraries for accessible behavior. This is the approach popularized by shadcn/ui.
TL;DR
- Repeated utility lists → extract a component (React, Vue, Svelte, Angular, or a template partial).
- Use
@applysparingly: for styling third-party markup or tiny element-level base styles. - Model component variants with cva or tailwind-variants for typed, readable variant APIs.
- Merge caller classes with
cn()(clsx+tailwind-merge) soclassName="px-8"overridespx-4predictably. - Auto-sort classes with prettier-plugin-tailwindcss, and lint with editor IntelliSense.
- Get behavior and accessibility from headless libraries (Radix, React Aria, Headless UI, Ark UI), and styling from Tailwind.
Quick Example
A button with typed variants, safe overrides, and headless composition (React):
Core Concepts
Extract Components, Not Classes
Tailwind's philosophy is that styling belongs with markup, and reuse happens at the component level. When the same utilities repeat:
- Within one file: loops, or a local variable holding the classes.
- Across the app: a component in your framework (
Button,Card,Badge,FormField). - For non-component templates (server-rendered partials, Markdown), a partial or macro.
This keeps styling co-located, lets you delete unused styles with the component, and avoids naming hundreds of CSS classes.
When @apply Makes Sense
@apply composes utilities into a CSS class:
Good uses: styling HTML you don't control (CMS or Markdown output, third-party widgets), and small base element styles. Overusing it recreates traditional CSS problems (naming, dead code, specificity) while losing co-location. In v4, files that use @apply with your theme outside the main stylesheet need @reference "../app.css";.
Variant APIs: cva and tailwind-variants
class-variance-authority (cva) and tailwind-variants define a base class plus typed variants, compoundVariants (styles for combinations such as variant=danger plus size=sm), and defaults. tailwind-variants adds slots for multi-part components (card header, body, and footer) and responsive variants. Both give components a clear, type-checked styling API.
Merging Classes Safely
Passing className="px-8" to a component that already has px-4 produces px-4 px-8, and which wins depends on stylesheet order, not class order. tailwind-merge understands Tailwind's utility groups and removes conflicting earlier classes, so the caller's px-8 wins. The cn() helper (twMerge(clsx(...))) is the standard pattern.
Headless Components
Accessible behavior (focus management, keyboard navigation, ARIA roles, dismiss on outside click) is hard to build correctly. Headless libraries provide it without styles:
Style them with Tailwind using the state they expose via data-* and aria-* attributes (see variants).
The shadcn/ui Model
shadcn/ui isn't a dependency: a CLI copies component source (built on Radix or other primitives, Tailwind, and cva) into your repo, so you own and customize it. The pattern (owned components, headless primitives, tokens as CSS variables, cn()) has become a common way to build Tailwind design systems quickly. See design systems.
Class Ordering and Tooling
- prettier-plugin-tailwindcss sorts classes in Tailwind's recommended order, which removes bike-shedding and makes diffs readable.
- Tailwind CSS IntelliSense (VS Code and others) provides autocomplete, hover previews, and linting for conflicting or invalid classes.
- Configure both to recognize your
cn(),cva(), andtv()calls.
Best Practices
Build a Small Set of Primitives
Buttons, inputs, cards, badges, dialogs, and layout primitives (Stack, Cluster, Grid) with variant APIs cover most UI. Pages compose primitives instead of repeating raw utilities.
Style With Semantic Tokens
Components should use semantic theme tokens (bg-surface, text-muted-foreground, ring-accent), not raw palette values, so theming and dark mode work without editing components. See theme configuration.
Keep Class Lists Readable
Group long lists with cva base arrays or line breaks, and let Prettier sort them. If a component's classes become unreadable, it's probably doing too much. Split it.
Document Components in Storybook
Showcase variants, states, and dark mode for each primitive, and test accessibility there. See Storybook.
Common Mistakes
@apply Everywhere
Converting every component into .btn { @apply … } classes brings back naming, specificity, and dead CSS, which defeats the point of utility-first CSS. Extract framework components instead.
Concatenating Classes Without Merging
Building Class Names Dynamically
` text-${size} ` isn't detected by Tailwind's scanner. Map variants to complete class strings (which is exactly what cva does).
FAQ
Is @apply bad practice in Tailwind?
Not inherently, but overusing it is. It's appropriate for styling markup you can't add classes to, and for small base styles. For reusable UI, extracting components in your framework is better: styles stay co-located, and component props provide variants.
What is tailwind-merge for?
It resolves conflicting Tailwind classes, keeping only the last one within the same utility group (for example the last padding class). That lets components accept className overrides that reliably win over their defaults.
Should I use cva or tailwind-variants?
Both provide typed variant APIs. cva is minimal and widely used (shadcn/ui uses it). tailwind-variants adds slots for multi-part components, responsive variants, and built-in merge integration. Pick one and use it consistently.
What is shadcn/ui?
A collection of accessible, Tailwind-styled components (mostly built on Radix primitives) that you copy into your project with a CLI rather than installing as a package. You own the code and customize it freely. It's a popular foundation for Tailwind design systems.
Related Topics
- Tailwind — The framework overview
- Tailwind Theme Configuration — Tokens components rely on
- Tailwind Variants — Styling states from data and aria attributes
- Design Systems — Building shared component libraries
- React — The most common home of these patterns
- Storybook — Documenting components