iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Component Patterns

Tailwind components pattern: when to extract, @apply trade-offs, cn helper, and the discipline that keeps utility-first sustainable.

Tailwind — components pattern

EXAMPLE
// ===== When utility-first goes wrong =====
// Markup like this every screen:
<button class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 focus:ring-2 focus:ring-blue-400">

// Repeated 50 times. Copy-paste rot. Inconsistent variations.

// ===== Solution 1: component =====
// Extract to a React/Vue/Svelte component:
function PrimaryButton({ className, ...props }) {
  return (
    <button
      className={cn('px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 focus:ring-2 focus:ring-blue-400', className)}
      {...props}
    />
  );
}

// Usage:
<PrimaryButton onClick={save}>Save</PrimaryButton>
<PrimaryButton className="w-full" disabled={loading}>Submit</PrimaryButton>

// ===== Solution 2: cva (class-variance-authority) =====
import { cva } from 'class-variance-authority';

const button = cva('rounded-md font-semibold focus:outline-none focus:ring-2', {
  variants: {
    intent: {
      primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-400',
      danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-400',
      ghost: 'bg-transparent text-slate-900 hover:bg-slate-100',
    },
    size: {
      sm: 'text-sm py-1 px-3',
      md: 'text-base py-2 px-4',
      lg: 'text-lg py-3 px-6',
    },
  },
  defaultVariants: { intent: 'primary', size: 'md' },
});

function Button({ intent, size, className, ...props }) {
  return <button className={cn(button({ intent, size }), className)} {...props} />;
}

// ===== Solution 3: @apply (use sparingly) =====
@layer components {
  .btn {
    @apply px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700;
  }
  .btn-danger { @apply bg-red-600 hover:bg-red-700; }
}

<button class="btn">Save</button>

// Trade-off: leaves the utility-first mindset; defeats some build optimisations.
// Useful for design tokens that appear EVERYWHERE without variants.

// ===== shadcn/ui pattern (recommended) =====
// - Copy components into YOUR codebase (not a dependency)
// - Use cva for variants
// - className override always supported
// - Use Radix UI for accessible primitives + Tailwind for styling

// ===== When to extract =====
// Rule of three: extract a component when the pattern appears for the THIRD time.
// Before that, copy-paste is fine.

// ===== What to leave inline =====
// One-off layouts:
<div className="flex items-center gap-2 p-4">
// These are clearer inline than as 'CardHeader' helpers.

// ===== Patterns to internalise =====
// - Component for repeated UI (button, input, card)
// - cva for variants (intent / size / state)
// - className prop on every component for parent overrides
// - @apply only for truly universal design tokens
// - shadcn/ui style: copy + own the components

// ===== Pitfalls =====
// - Premature extraction -> wrong abstraction
// - @apply everywhere -> reinvents CSS
// - Component without className override -> consumers fight the styles
// - Long class strings without cn -> conflicts (use twMerge)

Why it matters

Utility-first stays sustainable when you extract repeated patterns into components, parameterise with cva variants, allow className overrides, and reach for @apply only sparingly. The shadcn/ui pattern (copy + own + cn helper) became the default for a reason.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// React: extract a reusable Button component, not 50 classes per call site.
const Button = ({ children }) => <button className="px-4 py-2 bg-emerald-600 text-white rounded">{children}</button>;
Try it Yourself »

Discussion

Loading…