Reuse: components, @apply and class merging

Tailwind CSS · Advanced · 12 min

Long repeated class lists are the main maintenance cost of Tailwind, and these patterns keep it under control.

The answer to "my class lists repeat" is usually a component, not CSS. Put the markup in a React, Vue or Svelte component, a template partial, or even a loop, and the classes exist once.

Variants of a component (primary/secondary, sm/lg) are best expressed as maps from props to class strings. Libraries such as cva (class-variance-authority) formalise this. Two helpers are widely used:

  • clsx joins classes conditionally: clsx("px-4", active && "bg-indigo-600").
  • tailwind-merge resolves conflicts so overrides win: twMerge("px-4 py-2", "px-6") gives "py-2 px-6". Without it, both px-4 and px-6 would be present and the winner would depend on stylesheet order, not on the order in the class attribute.

@apply inlines utilities into your own CSS: .btn { @apply rounded-md px-4 py-2; }. It's reasonable for HTML you don't control (CMS output, third-party widgets) or very small shared pieces. Overusing it recreates traditional CSS, with naming and growing stylesheets, and gives up the benefits.

In v4, component style blocks (Vue <style>, Svelte, CSS modules) are processed separately, so @apply and theme variables there need @reference "../app.css"; at the top, which imports the theme for reference without duplicating CSS.

Keep class order consistent with the Prettier plugin; it makes long lists far easier to scan.

Example

// Button.jsx  (npm install clsx tailwind-merge)
import clsx from "clsx";
import { twMerge } from "tailwind-merge";

const variants = {
  primary: "bg-indigo-600 text-white hover:bg-indigo-700",
  ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
};
const sizes = { sm: "px-2.5 py-1 text-sm", md: "px-4 py-2" };

export function Button({ variant = "primary", size = "md", className, ...props }) {
  return (
    <button
      className={twMerge(clsx("rounded-md font-medium", variants[variant], sizes[size]), className)}
      {...props}
    />
  );
}
// <Button size="sm" className="px-6">Save</Button>
// -> "rounded-md font-medium bg-indigo-600 ... py-1 text-sm px-6" (px-2.5 removed)

Remember

  • Extract components first; @apply second
  • Model component variants as maps from props to classes
  • tailwind-merge makes later conflicting classes win
  • Vue/Svelte style blocks need @reference to use @apply

Watch out for

  • Passing className="px-6" to a component that already has px-4 without merging; which one wins depends on CSS order.

Check yourself

  1. Without tailwind-merge, an element has class="px-6 px-4". Which padding applies?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: tailwindcss.com/docs/styling-with-utility-classes#managing-duplication

Builds on

Unlocks

Nothing builds on this yet.