Updated June 2026. Tested on Tailwind CSS 3 and 4. Part of the Techalyst front-end series.

Tailwind is a utility-first CSS framework. It ships no buttons, no cards, no components, just a large set of single-purpose classes you compose to build your own designs. That sounds like more typing than a component library, and it is at first, but the control and consistency you get in return are why it has taken over front-end styling. Here is how it actually works.

Utility-first, in one comparison

A traditional library hands you a finished, hard-to-customise component. Tailwind hands you the CSS building blocks and gets out of the way.

<!-- component library: pre-styled, limited control -->
<Button variant="primary">Save</Button>

<!-- tailwind: composed from utilities, total control -->
<button class="bg-sky-600 text-white px-4 py-2 rounded hover:bg-sky-700">
  Save
</button>

The insight behind Tailwind is that what you usually need is not reusable components but reusable styles. Each class controls one thing (px-4 sets horizontal padding, rounded sets border radius), and you assemble them. Because the classes are professionally chosen, even without design skills you end up with consistent, decent-looking UI.

Reading a class name

Tailwind class names follow a simple pattern: a prefix for the CSS property, then a value.

text-xl
└──┘ └┘
     └─ the value or size
 └────── which property (here, font size)

Sizes use a numeric scale built on rem, where the default root is 16px, so p-1 is 0.25rem (4px), p-4 is 1rem (16px), p-8 is 2rem (32px). The same scale runs through margin (m-*), width, height, and gap, which is what keeps spacing consistent across a whole app without you tracking pixel values. A handy spacing trick is space-x-* / space-y-* on a parent, which inserts a gap between children without touching each child.

Variants: conditional styling

Variants are Tailwind's version of pseudo-selectors and media queries. Prefix a class and it applies only when the condition holds, think of it as an inline if.

<button class="bg-sky-600 hover:bg-sky-700 focus:ring-2 disabled:opacity-50">
  Click
</button>

hover:, focus:, active:, disabled:, checked:, and dark: are the everyday state variants, and they stack freely on one element. The variant class is added when its condition is true and removed when it is not, all in CSS, no JavaScript.

Responsive design is mobile-first

The breakpoint variants (sm:, md:, lg:, xl:, 2xl:) are the most important use of variants. Tailwind is mobile-first: an unprefixed class applies at every width, and a breakpoint prefix means "this width and up".

<!-- stacked on mobile, side by side from md upward -->
<div class="flex flex-col md:flex-row gap-4">
  <aside class="w-full md:w-1/3">Sidebar</aside>
  <main class="w-full md:w-2/3">Content</main>
</div>

<!-- hide on mobile, show from md; the inverse for a hamburger -->
<nav class="hidden md:block">Desktop nav</nav>
<button class="block md:hidden">Menu</button>
Variant Min width Typical target
sm: 640px large phones
md: 768px tablets
lg: 1024px laptops
xl: 1280px desktops
2xl: 1536px wide screens

One rule to internalise: breakpoints never auto-cancel. sm:text-red-500 stays red at every width above 640px, so to change it again at a larger size you must explicitly override it (lg:text-black). Design from the smallest screen up and layer larger breakpoints on top.

Extracting repeated patterns with @apply

Long class lists repeated across many elements get unwieldy. When a combination recurs, pull it into a real class with @apply, which copies the utilities' CSS into your selector at build time.

.btn-primary {
  @apply bg-sky-600 text-white px-4 py-2 rounded hover:bg-sky-700;
}
<button class="btn-primary">Save</button>

Use this sparingly. The Tailwind philosophy is to keep utilities in the markup (it co-locates style with structure and avoids naming things), and reach for @apply only when a pattern truly repeats. In component frameworks like Vue, a reusable styled component is often a better answer than an @apply class anyway.

Theming: your own colors and scales

When the built-in values are not enough, extend the theme rather than fighting it. The key habit is using extend, which adds to Tailwind's defaults, instead of replacing them wholesale.

// tailwind.config.js (Tailwind 3)
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: '#0ea5e9',
        'brand-dark': '#0369a1',
      },
    },
  },
}

Define a color once and it becomes available across every color utility at once, text-brand, bg-brand, border-brand, ring-brand, and it picks up opacity modifiers (bg-brand/75) for free. Tailwind 4 moves this configuration into CSS itself with @theme, but the mental model is identical: extend the scale, follow the naming conventions, and your custom values behave exactly like the built-in ones.

A note on build size

Tailwind generates only the classes you actually use, scanning your template files and emitting a tiny stylesheet. In Tailwind 3 you list those files in the content array; Tailwind 4 detects them automatically. Either way the production CSS is typically a few kilobytes, not the multi-megabyte file you would get if every possible class were shipped.

Wrapping up

Tailwind styles by composing single-purpose utilities directly in your markup, which trades a little verbosity for a lot of control and consistency. Learn the class-name pattern and the rem-based spacing scale, lean on state and responsive variants (remembering mobile-first and that breakpoints never auto-cancel), and reach for @apply or a real component only when a pattern genuinely repeats. Extend the theme with extend for custom colors and scales, and let the build step keep your CSS tiny.

More in the series: class and style bindings in Vue 3 and single file components and script setup. Questions welcome below.