Lesson 2 · Frontend for backend developers
The first time you see it you will think it is a step backwards. Here is the argument for why it isn't.
You will open a file at work and find this:
<div className="flex items-center gap-3 rounded-lg border
border-slate-200 p-4 hover:bg-slate-50">
The instinct — "that's inline styles with extra steps, we solved this in 2003" — is the correct instinct to have and the wrong conclusion to reach. Deal with the objection first, because until it is dealt with you will not use the tool honestly.
Traditional CSS asks you to name things. A component needs a style, so you
invent .card__header--compact, put it in a stylesheet, and now you
own a second file that must be kept in step with the first. Three things then go
wrong, reliably:
.card to fix
one page silently changes four other pages. CSS is a global namespace with no
imports.Tailwind's bet is the same one behind from x import y over
from x import *: locality beats abstraction until
the abstraction has actually earned its keep. Everything affecting this element
is visible on this element. Delete the element, the styling is gone with it —
no orphans, no grep.
One class, one declaration. That's the whole model.
| Class | Means |
|---|---|
p-4 | padding: 1rem — the scale is in units of 0.25rem, so 4 × 0.25 = 1rem |
flex | display: flex |
gap-3 | gap: 0.75rem |
text-slate-500 | a specific grey from a designed palette |
hover:bg-slate-50 | background on hover — a CSS state you cannot express in an inline style at all |
md:flex-row | only at medium screens and wider — likewise impossible inline |
Those last two rows are the actual answer to "this is just inline styles."
Inline styles cannot do hover, focus, media queries, or dark mode. Utility
classes can, because they are real CSS rules. And the numbers are not free-form:
p-4 comes from a fixed scale, so a codebase with fifty developers
cannot accumulate padding values of 13px, 14px and 15px.
Which claim about Tailwind utility classes is actually true?
Tailwind v4 changed installation completely. Almost every blog post and
Stack Overflow answer you will find describes v3: a
tailwind.config.js, a postcss.config.js, and
@tailwind base; directives. None of that applies. If a tutorial
tells you to run npx tailwindcss init, it is out of date —
close it.
Install, in the Vite project from Lesson 1.
npm install tailwindcss @tailwindcss/vite
Register the plugin. Tailwind now runs as part of Vite's pipeline — no separate PostCSS layer.
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
Import it in your CSS entry file. One line, replacing whatever is there.
src/index.css
@import "tailwindcss";
That is the entire setup. There is no config file until you want one — and when you do, it is CSS, not JavaScript:
@import "tailwindcss";
@theme {
--color-brand: #8a3324; /* now usable as bg-brand, text-brand, ... */
}
Sooner or later you will repeat the same twelve classes on eight buttons and think: I should extract a CSS class for this.
Don't. In a component-based codebase you already have a
mechanism for reuse, and it is better than a CSS class because it can carry
behaviour and types as well as styling. Extract a <Button>
component, not a .btn class. That is Lesson 3, and it is
the reason Tailwind and React are so often seen together: Tailwind deliberately
gives you no good abstraction mechanism, because the component is the
abstraction mechanism.
In src/App.tsx, delete everything in the returned markup and
build a centred card using only utility classes. Target:
Do not guess class names — use the search on tailwindcss.com/docs. Learning to find a utility fast is more valuable than memorising thirty of them.
Before you build it: what do you expect mx-auto to
do, and why will it do nothing without a width constraint?
mx-auto sets margin-left and
margin-right to auto, which centres a block element
inside its parent. A block element with no width constraint already fills its
parent, so there is no leftover space for the auto margins to split — nothing
visibly moves. Pair it with max-w-sm and the centring appears.