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.
