Concept
Tailwind CSS is a utility-first CSS framework, rather than writing semantic component classes (.card) with corresponding CSS rules, you compose a design directly in markup from small, single-purpose utility classes (flex, p-4, text-lg, bg-indigo-600). This is a genuinely different authoring model from BEM/ITCSS/CSS Modules/CSS-in-JS, not just a different syntax for the same thing, the styling decisions live in the markup, not in a separate stylesheet mapped by class name.
Utility-first, in practice
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
<h2 class="text-lg font-semibold text-gray-900">Card title</h2>
<button class="px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">
Action
</button>
</div>No .card class, no separate CSS file to cross-reference, the entire visual design of this element is legible directly from its markup. This is Tailwind's central trade-off: you give up semantic class names and separation of "structure" (HTML) from "presentation" (CSS) in the traditional sense, in exchange for never leaving the file you're editing, and, critically, never having to name things (a genuinely time-consuming, bikeshedding-prone part of traditional CSS authoring: "is this .card-header or .card__title or .card-heading?").
The build-time purge/JIT engine, Tailwind's actual technical core
Tailwind isn't just a giant pre-written CSS file, modern Tailwind uses a Just-In-Time engine that scans your actual source files for class names used and generates only the CSS for classes that are genuinely present in your codebase. This is what keeps production bundle size small despite Tailwind's utility set being enormous (thousands of possible class combinations), an unused utility never makes it into the shipped CSS at all.
// tailwind.config.js, content sources the JIT engine scans
export default {
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
theme: { /* design tokens: colors, spacing, breakpoints */ },
};Design tokens via config, not arbitrary values
<!-- Uses design-system-constrained values from tailwind.config.js -->
<div class="p-4 text-lg bg-indigo-600">
<!-- Arbitrary value escape hatch, bypasses the design system, use sparingly -->
<div class="p-[17px] text-[22px] bg-[#4f46e5]">Tailwind's default scale (p-1 through p-96, a fixed color palette, fixed font sizes) is itself a design-system-enforcement mechanism, engineers can only pick from the configured scale by default, which naturally prevents the "17px here, 18px there" inconsistency that free-form CSS authoring allows. The square-bracket arbitrary value syntax is an intentional escape hatch for one-off cases, but reaching for it constantly defeats the actual benefit of having a constrained scale in the first place.
Responsive and state variants as class prefixes
<div class="text-sm md:text-base lg:text-lg hover:bg-gray-100 dark:bg-gray-800 focus:ring-2">Responsive breakpoints (md:, lg:), pseudo-class states (hover:, focus:, disabled:), and dark mode (dark:) are expressed as class prefixes rather than separate @media/:hover blocks in a different file, keeping the entire responsive/interactive behavior of an element visible at the point of use, at the cost of longer class lists.
Extracting repeated utility combinations
// React: the natural "component" extraction point for Tailwind, a component, not a CSS class
function Button({ children }) {
return (
<button className="px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">
{children}
</button>
);
}/* Alternative: @apply for genuinely reusable, framework-agnostic patterns */
.btn-primary {
@apply px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700;
}In a component-based framework (React, Vue), the idiomatic way to avoid repeating a long utility class list is extracting a component, not a CSS class, the component is the reusable unit, matching how Tailwind is designed to be used. @apply (compiling a set of utilities into a named CSS class) exists for cases without component abstraction available, but Tailwind's own docs increasingly de-emphasize it in favor of component extraction as the primary reuse mechanism.
Common Mistakes
1. Fighting the design system with arbitrary values constantly
<!-- Defeats the whole point of a constrained scale -->
<div class="p-[13px] m-[7px] text-[15.5px]">Reaching for [arbitrary] values as a first resort rather than a rare escape hatch reintroduces exactly the "inconsistent hand-picked values everywhere" problem the constrained scale exists to prevent. If the design genuinely needs a value outside the scale often, extend the tailwind.config.js theme instead of scattering arbitrary values throughout markup.
2. Extracting @apply classes for everything instead of using component composition
In a component-based framework, defaulting to @apply-based CSS classes rather than component extraction re-introduces the separate-stylesheet-to-cross-reference problem Tailwind was largely adopted to avoid, without gaining much, component extraction is generally the more idiomatic, more powerful (can also encapsulate JSX structure/logic, not just class strings) reuse mechanism in a framework context.
3. Not configuring the JIT content paths correctly, causing missing styles in production
// Wrong, misses files, causing classes used there to be purged/missing in the production build
content: ["./src/pages/**/*.tsx"], // forgot components/, layouts/, etc.A common, confusing production-only bug: styles that work fine in dev (where Tailwind's dev mode is sometimes more permissive) disappear in the production build because the content glob pattern doesn't actually cover every file where Tailwind classes are used, always verify the content configuration covers the full source tree.
4. Dynamically constructing class name strings, breaking the JIT scanner
// Wrong, JIT engine scans for literal class name strings; this dynamic construction is invisible to it
const colorClass = `bg-${color}-500`;
// Right, literal, scannable class names, chosen via a lookup/conditional
const colorClasses = { red: "bg-red-500", blue: "bg-blue-500" };
const colorClass = colorClasses[color];The JIT engine works by statically scanning source files for literal class name substrings, a dynamically interpolated class name (`bg-${color}-500`) is never actually present as a complete string anywhere in the source, so the scanner can't find it and the corresponding CSS never gets generated, causing a silent missing-style bug in production specifically (dev mode with the full CSS available can mask this).
5. Assuming utility-first means "no architecture/discipline needed"
Utility classes solve the naming/scoping problem, but a large Tailwind codebase still benefits from genuine component composition discipline, a well-configured design token scale, and consistent patterns for when to extract a component versus repeat utilities, utility-first isn't a substitute for architectural thinking, just a different substrate for it.
Best Practices
- Stay within the configured design token scale; treat arbitrary
[value]syntax as a rare escape hatch, not a default. - Extract components (in a component-based framework) as the primary reuse mechanism, not
@apply-based CSS classes, for anything with real internal logic/structure beyond a flat class list. - Verify
contentglob configuration covers the entire actual source tree, especially after adding new directories/file types to a project. - Never dynamically interpolate class name strings, use a lookup object/conditional mapping to complete, literal class names instead, so the JIT scanner can find them.
- Extend
tailwind.config.js's theme for values a project genuinely needs repeatedly, rather than scattering arbitrary values. - Combine with genuine component architecture discipline, utility-first isn't a substitute for thinking about reusable UI structure.
Further Resources
- Tailwind CSS, official docs
- Tailwind CSS, Just-in-Time engine
- Tailwind CSS, reusing styles / @apply guidance
- Adam Wathan (Tailwind creator), CSS Utility Classes and "Separation of Concerns", the original case for utility-first.
- Tailwind CSS, configuration reference
