π¬οΈ Tailwind CSS Utility-First Approach
Tailwind CSS flips the usual workflow: instead of writing CSS in a separate file and inventing class names, you compose designs from small, single-purpose utility classes right in your HTML. It sounds noisy at first, then becomes surprisingly fast and maintainable. This lesson covers the philosophy, a modern setup, the utility system, and how to build real components β plus a clear-eyed comparison with Bootstrap.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain the utility-first philosophy and how it differs from writing traditional CSS
- Set up Tailwind with a modern build and understand the CDN option for prototyping
- Read and write utilities using Tailwind's consistent naming system
- Apply responsive (
md:) and state (hover:,focus:,dark:) variants - Build a real component and extract repeated patterns to keep markup clean
- Decide when to choose Tailwind over Bootstrap and vice versa
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Build a responsive pricing card with utilities, then refactor a bloated element into a reusable class.
In This Lesson
The Utility-First Philosophy
Tailwind CSS is a utility-first framework: it gives you low-level, single-purpose classes β p-6 for padding, text-xl for font size, bg-white for background β that you combine directly in your markup to build any design. Unlike Bootstrap, it ships no pre-designed components. You assemble the look yourself, without ever opening a separate CSS file.
Traditional CSS
/* styles.css */
.card {
background-color: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
<div class="card">
<h2 class="card-title">Card Title</h2>
<p>Card content goes here.</p>
</div>
Tailwind CSS
<!-- No separate CSS file, no invented class names -->
<div class="bg-white rounded-lg p-6 shadow-md">
<h2 class="text-xl font-semibold mb-3">Card Title</h2>
<p>Card content goes here.</p>
</div>
π Analogy: Tailwind as LEGO Bricks
A component framework is a pre-assembled toy β ready to play with, hard to reshape. Tailwind is a box of LEGO bricks: small standardized pieces you connect in countless ways to build exactly what you imagine. You trade "instant finished object" for "build anything," and the pieces always fit because they share one design system.
π‘ Why not just use inline styles?
Because utilities pull from a constrained design system β a fixed spacing scale, a curated color palette, consistent type sizes. p-4 is always the same padding everywhere; style="padding: 17px" is a one-off that invites drift. Utilities also support things inline styles can't: hover states, media queries, and dark mode.
Getting Started
Tailwind is a build tool at heart: it scans your files for class names and generates a stylesheet containing only the utilities you actually used. That's why production Tailwind bundles are tiny.
Modern install (Tailwind v4 with Vite)
The current major version, Tailwind v4, uses a first-class Vite plugin and a CSS-first setup β most configuration now lives in your CSS instead of a JavaScript config file.
# In a Vite project
npm install tailwindcss @tailwindcss/vite
// vite.config.js
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss()],
});
/* src/style.css β one line pulls in Tailwind */
@import "tailwindcss";
/* Customize the design system right here, no JS config needed */
@theme {
--color-brand: #1992d4;
--font-display: "Poppins", sans-serif;
}
CDN β for quick prototyping only
To experiment without a build step, load the browser build. It compiles utilities in the browser, so it's great for demos but not for production.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tailwind Demo</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body>
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md">
<div class="text-xl font-medium text-black">Hello Tailwind!</div>
<p class="text-gray-500">Using the browser build for a quick prototype.</p>
</div>
</body>
</html>
β οΈ Version note
Many older tutorials show a tailwind.config.js file, npx tailwindcss init, and @tailwind base; @tailwind components; @tailwind utilities; directives. That's the Tailwind v3 workflow β still common and perfectly valid β but v4 replaces it with the single @import "tailwindcss"; line and the @theme block shown above. The utility classes themselves are the same, so everything you learn here transfers.
The Utility Naming System
Tailwind's power comes from a predictable naming convention. Once you learn the pattern, you can guess most class names without reaching for the docs.
| Category | Prefix | Example | CSS equivalent |
|---|---|---|---|
| Padding | p-, px-, py-, pt-β¦ | p-4 | padding: 1rem; |
| Margin | m-, mx-, mt-β¦ | mt-2 | margin-top: 0.5rem; |
| Width / Height | w-, h- | w-1/2 | width: 50%; |
| Font size | text- | text-lg | font-size: 1.125rem; |
| Text color | text- | text-blue-500 | color: #3b82f6; |
| Background | bg- | bg-gray-100 | background-color: #f3f4f6; |
| Border | border, border-t⦠| border-2 | border-width: 2px; |
| Flexbox | flex, items-, justify- | flex items-center | display: flex; align-items: center; |
π The spacing scale
Numbers like the 4 in p-4 are steps on a scale, not pixels. Each step is 0.25rem (4px by default), so p-4 = 1rem, p-8 = 2rem. This shared scale is what keeps a whole site's spacing visually consistent.
Arbitrary values, when you truly need them
For the rare one-off outside the scale, square-bracket syntax lets you drop in an exact value without leaving your markup:
<div class="top-[117px] bg-[#1da1f2] w-[32rem]">
Precise, escape-hatch values
</div>
Responsive & State Variants
Variants are prefixes that apply a utility only under certain conditions. This is how Tailwind handles responsiveness, hover, focus, dark mode, and more β all in the markup.
Responsive: mobile-first prefixes
Tailwind is mobile-first. An unprefixed utility applies everywhere; a prefixed one applies from that breakpoint up.
| Prefix | Applies from | Example |
|---|---|---|
| (none) | 0px (all sizes) | p-4 |
sm: | 640px | sm:p-6 |
md: | 768px | md:p-8 |
lg: | 1024px | lg:p-10 |
xl: | 1280px | xl:p-12 |
2xl: | 1536px | 2xl:p-16 |
<!-- Full width on phones, half on tablets, one-third on desktops -->
<div class="w-full md:w-1/2 lg:w-1/3 p-4 md:p-6">
Responsive box
</div>
State variants
| Variant | Example | When it applies |
|---|---|---|
hover: | hover:bg-blue-700 | Pointer is over the element |
focus: | focus:ring-2 | Element is focused |
active: | active:bg-blue-800 | Element is being pressed |
disabled: | disabled:opacity-50 | Element is disabled |
dark: | dark:bg-gray-800 | Dark mode is active |
group-hover: | group-hover:text-white | An ancestor marked group is hovered |
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded
focus:outline-none focus:ring-2 focus:ring-blue-400">
Hover and focus me
</button>
β Variants stack
You can combine prefixes: md:hover:bg-blue-700 applies the hover background only on medium screens and up, and dark:hover:bg-gray-700 handles hover in dark mode. Order reads left to right as "under these conditions, apply this."
Building a Real Component
Let's build a responsive profile card that puts the naming system and variants together.
<div class="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden
md:flex md:max-w-2xl">
<div class="md:shrink-0">
<img class="h-48 w-full object-cover md:h-full md:w-48"
src="profile.jpg" alt="Team member portrait">
</div>
<div class="p-8">
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
Engineering
</div>
<a href="#" class="block mt-1 text-lg font-medium text-black hover:underline">
Carla Lim
</a>
<p class="mt-2 text-gray-500">
Frontend engineer focused on accessibility and the design system.
</p>
<button class="mt-4 bg-indigo-600 hover:bg-indigo-700 text-white text-sm
font-medium py-2 px-4 rounded">
View profile
</button>
</div>
</div>
π‘ Read it like a sentence
The outer element says: "at most small-width, centered, white, rounded, shadowed, hidden overflow β and from medium up, become a flex row up to 2xl wide." Everything about the layout is legible in one place, no jumping to a stylesheet to find out what .profile-card does.
Layout utilities: flex and grid
<!-- Center content both axes -->
<div class="flex justify-center items-center h-64 bg-gray-100">
<div class="bg-white p-6 rounded shadow">Centered</div>
</div>
<!-- Responsive grid: 1 column on phones up to 4 on desktops -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-blue-100 p-4">Item 1</div>
<div class="bg-blue-100 p-4">Item 2</div>
<div class="bg-blue-100 p-4">Item 3</div>
<div class="bg-blue-100 p-4">Item 4</div>
</div>
Keeping Markup Clean
The most common complaint about Tailwind is "class bloat" β long chains of utilities repeated across many elements. There are two disciplined answers.
1. Extract a component (the preferred way)
In React, Vue, or any component framework, you already have the perfect abstraction: wrap the markup in a component and reuse that, not a copied string of classes.
function PrimaryButton({ children, ...props }) {
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
{...props}
>
{children}
</button>
);
}
// Reuse it: <PrimaryButton>Save</PrimaryButton>
2. Extract a class with @apply
When you're not in a component framework, the @apply directive folds a set of utilities into a single class in your CSS:
@import "tailwindcss";
.btn-primary {
@apply inline-block px-4 py-2 rounded font-semibold
bg-blue-500 text-white hover:bg-blue-700;
}
<button class="btn-primary">Save</button>
β οΈ Don't reach for @apply too early
Tailwind's own guidance is to prefer real components over @apply. Wrapping every button in a class recreates exactly the indirection Tailwind was meant to avoid β you're back to jumping between HTML and CSS. Extract only genuinely repeated, stable patterns.
Best practices at a glance
β Do
- Order classes consistently (layout β box β typography β color) for readability
- Design mobile-first, then add
md:/lg:refinements - Extract repeated UI into components; lean on the design tokens
- Mind accessibility β real focus states, sufficient contrast, semantic HTML
β οΈ Don't
- Copy-paste 20-class strings across a dozen elements
- Fight the design system with arbitrary values everywhere
- Forget hover/focus states just because they take another prefix
- Ship the browser CDN build to production
Tailwind vs. Bootstrap
These two are the poster children for the two philosophies you met last lesson. Here's how they actually differ.
| Aspect | Tailwind CSS | Bootstrap |
|---|---|---|
| Philosophy | Utility-first, low-level classes | Component-first, pre-designed elements |
| Default look | None β you design it | A distinct "Bootstrap look" out of the box |
| Learning curve | Steeper at first, easier to customize | Easy at first, harder to deeply customize |
| Bundle size | Tiny (only used utilities) | Larger even after trimming |
| JavaScript | None included | Modals, dropdowns, etc. included |
| Customization | Through the theme / CSS config | Through Sass variables |
π‘ Choose Tailwind whenβ¦
- Your design is bespoke and deviates from any framework's defaults
- You're implementing a custom design system or brand
- Bundle size and performance are priorities
- You want granular control and are working in React / Vue / Svelte
π‘ Choose Bootstrap whenβ¦
- You need a working UI very fast
- You want pre-built, accessible interactive components (modals, carousels)
- Your team already knows it, or you have no dedicated designer
- A conventional, standard-looking UI is perfectly fine
Hands-on Exercise
ποΈ Build a Pricing Card, Then Refactor
Objective: Practice composing utilities and applying the "keep markup clean" discipline.
Part A β Build
- Using the browser CDN, build a pricing card: a plan name, a big price, three feature lines, and a "Choose plan" button.
- Give it a white background, rounded corners, a shadow, and generous padding.
- Add a
hover:state to the button and a focus ring for keyboard users.
Part B β Refactor
Your button ended up with a long class string that you now need on three cards. Extract it into a reusable .btn-plan class with @apply.
π‘ Hint
Structure the card as bg-white rounded-2xl shadow-lg p-8 max-w-sm. Make the price large with text-4xl font-extrabold. Use a space-y-2 wrapper or a <ul> for the feature list. For the button start from w-full mt-6 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 rounded-lg focus:ring-2 focus:ring-indigo-400.
β Sample solution
<div class="max-w-sm mx-auto bg-white rounded-2xl shadow-lg p-8">
<h3 class="text-lg font-semibold text-gray-700">Pro</h3>
<p class="mt-2">
<span class="text-4xl font-extrabold">$29</span>
<span class="text-gray-500">/mo</span>
</p>
<ul class="mt-6 space-y-2 text-gray-600">
<li>β Unlimited projects</li>
<li>β Priority support</li>
<li>β Advanced analytics</li>
</ul>
<button class="btn-plan">Choose plan</button>
</div>
Part B β the extracted class:
@import "tailwindcss";
.btn-plan {
@apply w-full mt-6 bg-indigo-600 hover:bg-indigo-700 text-white
font-semibold py-2 rounded-lg
focus:outline-none focus:ring-2 focus:ring-indigo-400;
}
Now all three pricing cards share one source of truth for the button, and the markup reads cleanly.
π― Quick Quiz
Question 1: What does the utility-first approach mean in Tailwind?
Question 2: Tailwind's responsive system is mobile-first. What does md:w-1/2 do?
Question 3: What is the recommended first choice for reducing repeated utility strings?
Summary & Quiz
π Key Takeaways
- Tailwind is utility-first: compose designs from small single-purpose classes in your markup β no pre-built components.
- It's a build tool that emits only the utilities you use, so production bundles are tiny; modern v4 uses a CSS-first
@import "tailwindcss";setup. - The naming system (property-value, on a shared spacing/color scale) makes class names predictable.
- Variants add responsiveness (
md:) and states (hover:,focus:,dark:) right in the markup, and they stack. - Keep markup clean by extracting components first, and
@applyonly for stable repeated patterns. - Choose Tailwind for custom designs and performance; choose Bootstrap for speed and pre-built components.
π Further Reading
- Tailwind CSS β official documentation
- Tailwind β the utility-first fundamentals
- Tailwind Play β official playground
- Tailwind Plus (Tailwind UI) β component examples
- Flowbite β open-source Tailwind components
π What's Next?
You've now seen both philosophies in depth. Next we go hands-on with the most-used grid in the world: Bootstrap Grid System Mastery, digging into containers, gutters, alignment, and advanced column control.
π Excellent work!
Utility-first thinking is a skill that pays off across the whole modern frontend. Let's return to Bootstrap and master its grid.