Tailwind CSS Tips: Patterns You Should Be Using
Level up your Tailwind CSS workflow with utility composition patterns, custom themes, responsive techniques, and productivity tips for modern UIs.
Tailwind CSS has fundamentally changed how many developers write CSS. But there is a significant difference between using Tailwind and using Tailwind well. Many developers stop at basic utility classes and miss the patterns that make Tailwind truly productive. This guide covers the techniques that experienced Tailwind users rely on daily.
Utility Composition Patterns
The Component Pattern with @apply
When a set of utilities repeats across many elements, extract it with @apply:
/* In your CSS file */
@layer components {
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;
}
.btn-primary {
@apply btn bg-blue-600 text-white hover:bg-blue-700;
}
.btn-secondary {
@apply btn bg-gray-200 text-gray-800 hover:bg-gray-300;
}
}
When to use @apply: Only extract utilities when a group of classes represents a reusable component (buttons, cards, badges). Do not extract every combination — that defeats the purpose of utility-first CSS.
When not to use @apply: One-off layouts, page-specific styling, or anything that only appears once. Just use the utilities inline.
Component Libraries with Props
In React, Vue, or Svelte, build component variants with conditional classes:
function Button({ variant = 'primary', size = 'md', children, ...props }) {
const baseClasses = 'rounded-lg font-medium transition-colors duration-200';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button className={`${baseClasses} ${variants[variant]} ${sizes[size]}`} {...props}>
{children}
</button>
);
}
For more complex variant logic, use the class-variance-authority (CVA) library:
import { cva } from 'class-variance-authority';
const button = cva('rounded-lg font-medium transition-colors duration-200', {
variants: {
intent: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
intent: 'primary',
size: 'md',
},
});
Custom Themes
Extending the Default Theme
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a5f',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
borderRadius: {
'4xl': '2rem',
},
},
},
};
Use extend to add values without removing Tailwind’s defaults. Only override the full key (without extend) when you want to replace the default scale entirely.
CSS Custom Properties for Dynamic Theming
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
surface: 'hsl(var(--color-surface) / <alpha-value>)',
'on-surface': 'hsl(var(--color-on-surface) / <alpha-value>)',
primary: 'hsl(var(--color-primary) / <alpha-value>)',
},
},
},
};
/* Light theme */
:root {
--color-surface: 0 0% 100%;
--color-on-surface: 222 47% 11%;
--color-primary: 221 83% 53%;
}
/* Dark theme */
.dark {
--color-surface: 222 47% 11%;
--color-on-surface: 210 40% 98%;
--color-primary: 217 91% 60%;
}
Now bg-surface, text-on-surface, and text-primary automatically adapt to the current theme. This is more flexible than Tailwind’s built-in dark: variant for complex theming.
Responsive Design Patterns
Mobile-First Defaults
Tailwind is mobile-first. Unprefixed utilities apply to all screens, and breakpoint prefixes apply at that breakpoint and above:
<!-- Mobile: stacked, Tablet+: side by side, Desktop: 3 columns -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
Container Queries
Tailwind v3.4+ supports container queries for component-based responsiveness:
<div class="@container">
<div class="flex flex-col @md:flex-row gap-4">
<img class="w-full @md:w-48" src="..." alt="...">
<div>
<h3 class="text-lg @lg:text-xl">Title</h3>
<p>Description</p>
</div>
</div>
</div>
Container queries respond to the parent’s width, not the viewport. This makes components truly self-contained.
Responsive Typography
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
Responsive Heading
</h1>
Or use clamp() for fluid typography:
@layer utilities {
.text-fluid-lg {
font-size: clamp(1.25rem, 2vw + 0.5rem, 2rem);
}
}
Dark Mode
Class-Based Dark Mode
// tailwind.config.js
module.exports = {
darkMode: 'class',
};
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1 class="text-gray-800 dark:text-gray-200">Title</h1>
<p class="text-gray-600 dark:text-gray-400">Content</p>
</div>
Toggle the dark class on the <html> element with JavaScript:
document.documentElement.classList.toggle('dark');
Respecting System Preferences
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Apply on load
if (prefersDark && !localStorage.theme) {
document.documentElement.classList.add('dark');
}
Advanced Patterns
Group and Peer Modifiers
Style children based on parent state:
<a href="#" class="group block p-4 rounded-lg hover:bg-blue-50">
<h3 class="font-medium group-hover:text-blue-600">Card Title</h3>
<p class="text-gray-500 group-hover:text-gray-700">Description</p>
</a>
Style siblings based on another element’s state:
<label>
<input type="checkbox" class="peer sr-only">
<span class="peer-checked:text-blue-600 peer-checked:font-bold">
Toggle option
</span>
</label>
Arbitrary Values
When design tokens do not cover your exact need:
<div class="w-[calc(100%-2rem)] top-[117px] bg-[#1a1a2e] grid-cols-[1fr_auto_1fr]">
Use sparingly. If you find yourself using arbitrary values frequently, add them to your theme configuration instead.
Prose for Long-Form Content
The @tailwindcss/typography plugin styles rendered Markdown and HTML content:
<article class="prose prose-lg dark:prose-invert max-w-none">
<!-- Rendered Markdown content gets beautiful default styling -->
</article>
Performance Tips
Purging Unused Styles
Tailwind v3+ uses JIT mode by default, generating only the classes you use. Ensure your content configuration covers all template files:
module.exports = {
content: [
'./src/**/*.{html,js,jsx,ts,tsx,vue,svelte,astro}',
'./public/index.html',
],
};
Missing paths means missing styles in production. Extra paths means slightly slower builds but no impact on output.
Avoiding Class Duplication
Use the tailwind-merge library to handle class conflicts:
import { twMerge } from 'tailwind-merge';
// Later class wins
twMerge('px-4 py-2', 'px-6'); // 'py-2 px-6'
This is essential when components accept className props that may conflict with base styles.
Useful Tools
When building custom themes, picking the right colors is critical. Our Tailwind Color Picker helps you find and fine-tune Tailwind color values, showing the full shade palette and providing the exact class names and hex values. For rapid prototyping, the Tailwind Playground lets you experiment with Tailwind classes and see results instantly without setting up a project.
Key Takeaways
- Extract components with
@applyonly when you have true reuse — three or more identical instances - Build variant systems with CVA or simple conditional classes
- Use CSS custom properties with Tailwind for dynamic theming beyond light/dark
- Design mobile-first and add complexity at larger breakpoints
- Leverage
groupandpeermodifiers instead of custom JavaScript for interactive styling - Keep arbitrary values to a minimum — extend the theme instead
- Always verify your
contentpaths include all template files
Tailwind is at its best when you embrace the utility-first philosophy fully. Resist the urge to recreate traditional CSS patterns. Instead, build components from utilities, compose variants declaratively, and let the framework handle the CSS optimization.
Comments