webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Tailwind CSS · Lesson 5 of 6

Components and variants

Components instead of @apply, variants, cn/tailwind-merge, dynamic classes.

Updated

The problem

With Tailwind, the same set of classes (a button, a card) shows up dozens of times. How do you reuse them without copying them everywhere? There are several options — only one is recommended.

Option Example Verdict
copy-paste the same 10 classes in 30 places ✗ impossible to change
@apply in CSS .btn { @apply px-4 py-2 } ✗ you reinvent classic CSS, losing the benefits
a component <Button variant="primary"> ✓ recommended
loops / map {items.map(i => <li className="...">)} ✓ the classes appear only once in the code

Reuse = components, not @apply

/* ✗ you reinvent classic CSS, losing the benefits */
.btn { @apply rounded-md px-4 py-2 bg-accent; }
// ✓ the component is the unit of reuse
export function Button(props) {
  return <button className="rounded-md px-4 py-2 bg-accent text-white" {...props} />
}

@apply is only OK for styles you don't control through components (content from a CMS, external libraries).

Variants — the basic pattern

Exactly what shared/ui/button.tsx in this project does:

const variants = {
  primary: 'bg-accent text-accent-fg hover:bg-accent-hover',
  secondary: 'border border-line-strong bg-surface hover:bg-hover',
  ghost: 'text-muted hover:bg-hover',
}
const sizes = { sm: 'h-8 px-3', md: 'h-9 px-4' }

<button className={cn(base, variants[variant], sizes[size], className)} />

The variants are finite and named — you don't accept color="#f00" as a prop.

cn, clsx, tailwind-merge

  • clsx / a simple cn — joins classes conditionally.
  • tailwind-merge — resolves conflicts: twMerge('px-2', 'px-4') → 'px-4'. Useful when you allow a className from outside that overrides.
  • cva (class-variance-authority) / tailwind-variants — a declarative API for variants, useful when you have many combinations.

Dynamic classes — the trap

Tailwind detects classes by reading the files' text. It can't guess classes that are built:

`bg-${color}-500`                              // ✗ doesn't exist in the final CSS
const colors = { red: 'bg-red-500', blue: 'bg-blue-500' }
colors[color]                                  // ✓ complete classes, written literally

Styles from dynamic data

Values computed at runtime → style or a CSS variable:

<div className="w-(--progress) bg-success" style={{ '--progress': `${pct}%` }} />

Summary

  • The unit of reuse is the component, not @apply.
  • Finite, named variants in an object; cn for combining, tailwind-merge for conflicts.
  • Classes must be written in full in the code — Tailwind doesn't see classes built from pieces.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.