Skip to main content
Ganesh Joshi
Back to Blogs

Tailwind CSS best practices for maintainable UIs

February 9, 20264 min read
Tutorials
CSS or code on screen

Tailwind CSS provides utility classes for styling directly in your markup. It eliminates the need for separate CSS files and removes unused styles automatically. But without discipline, Tailwind projects can become hard to maintain. These practices keep projects readable and consistent.

Class ordering

Consistent class order makes scanning easier and produces cleaner diffs. Follow this order:

Category Examples
Layout flex, grid, block, hidden
Position relative, absolute, fixed, z-10
Box model w-full, h-64, m-4, p-6
Typography text-lg, font-bold, leading-6
Colors text-gray-900, bg-white
Borders border, rounded-lg, border-gray-200
Effects shadow-md, opacity-50
Transitions transition-all, duration-300
States hover:, focus:, active:
Responsive sm:, md:, lg:, xl:

Automated sorting

Use the official Prettier plugin:

npm install -D prettier-plugin-tailwindcss
// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"]
}

Classes sort automatically on save.

Use design tokens

Prefer theme values over arbitrary values:

// Good: uses theme
<div className="w-64 bg-slate-700 p-4">

// Avoid: arbitrary values
<div className="w-[256px] bg-[#334155] p-[16px]">

Theme values are:

  • Consistent across the project
  • Easy to change globally
  • Documented in your config

Reserve arbitrary values for genuine one-offs that don't belong in the design system.

Component extraction

When utilities repeat across files, extract a component:

// Before: repeated button styles
<button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
  Save
</button>
<button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
  Submit
</button>

// After: Button component
function Button({ children, ...props }) {
  return (
    <button
      className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      {...props}
    >
      {children}
    </button>
  );
}

Variants with cva

Use class-variance-authority for component variants:

npm install class-variance-authority
import { cva } from 'class-variance-authority';

const button = cva(
  'rounded-lg px-4 py-2 font-medium transition-colors',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
        ghost: 'bg-transparent hover:bg-gray-100',
      },
      size: {
        sm: 'text-sm px-3 py-1.5',
        md: 'text-base px-4 py-2',
        lg: 'text-lg px-6 py-3',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
);

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

When to use @apply

Use @apply for:

  • Base styles on native elements (buttons, inputs)
  • Styles that must be in CSS (third-party components)
  • Repeated patterns across many components
@layer components {
  .btn-primary {
    @apply rounded-lg bg-blue-600 px-4 py-2 text-white;
    @apply hover:bg-blue-700 focus:ring-2 focus:ring-blue-500;
  }
}

Avoid @apply for:

  • One-off styles (keep in markup)
  • Complex component styles (use React components)
  • Everything (defeats Tailwind's purpose)

Conditional classes

Use clsx or tailwind-merge for conditional logic:

npm install clsx tailwind-merge
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

function cn(...inputs) {
  return twMerge(clsx(inputs));
}

function Button({ variant, disabled, className }) {
  return (
    <button
      className={cn(
        'rounded-lg px-4 py-2',
        variant === 'primary' && 'bg-blue-600 text-white',
        variant === 'secondary' && 'bg-gray-200 text-gray-900',
        disabled && 'cursor-not-allowed opacity-50',
        className
      )}
    >
      Click me
    </button>
  );
}

tailwind-merge resolves class conflicts (e.g., bg-red-500 overrides bg-blue-500).

Responsive design

Design mobile-first:

<div className="
  flex flex-col        {/* Mobile: stack vertically */}
  md:flex-row          {/* Medium+: row layout */}
  gap-4
  p-4 md:p-8           {/* Larger padding on desktop */}
">
Breakpoint Min width Target
(default) 0px Mobile
sm: 640px Large phones
md: 768px Tablets
lg: 1024px Laptops
xl: 1280px Desktops
2xl: 1536px Large screens

Accessibility

Always include focus states:

<button className="
  bg-blue-600 text-white
  hover:bg-blue-700
  focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
">
  Submit
</button>

Use focus-visible: for keyboard-only focus:

<a className="
  text-blue-600
  focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
">
  Link
</a>

Dark mode

Use the dark: variant:

<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
  Content
</div>

Configure in tailwind.config.js:

module.exports = {
  darkMode: 'class', // or 'media' for system preference
};

Tailwind v4 considerations

Tailwind v4 (current) uses:

  • Native CSS with Lightning CSS
  • No PostCSS required
  • CSS-native configuration
  • Automatic content detection

Check the Tailwind v4 docs for current syntax.

Project organization

src/
├── components/
│   └── ui/
│       ├── Button.tsx    # Component with Tailwind
│       └── Input.tsx
├── styles/
│   └── globals.css       # @tailwind directives, @layer components
└── tailwind.config.js    # Theme extensions

Keep utility classes in components. Use globals.css for base styles and truly shared patterns.

Summary

Order classes consistently and use Prettier to automate sorting. Prefer theme tokens over arbitrary values. Extract components for repeated patterns. Use @apply sparingly for base element styles. Handle conditionals with clsx and tailwind-merge. Design mobile-first with responsive prefixes. Always include focus states for accessibility.

Frequently Asked Questions

Follow a consistent order: layout (display, position), box model (width, height, margin, padding), typography, colors, borders, then effects. Use the prettier-plugin-tailwindcss to sort automatically.

Use @apply sparingly for repeated patterns like button styles used in many places. Prefer extracting React or Vue components over long @apply blocks. Overusing @apply defeats Tailwind's benefits.

Extract repeated styles into components. Use clsx or tailwind-merge for conditional classes. Split complex class strings with template literals. Keep utility classes but reduce duplication.

Prefer theme values (spacing.4, colors.slate.700) for consistency. Use arbitrary values like w-[137px] only for one-off cases that don't fit the design system.

Start with mobile styles (no prefix), then add breakpoint prefixes (sm:, md:, lg:) as needed. Design mobile-first and layer on larger screen styles progressively.

Related Posts