Lesson 2 · Frontend for backend developers

Tailwind, and why the classes look insane

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.

The problem Tailwind is solving

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:

In your language

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.

So what is a utility class

One class, one declaration. That's the whole model.

ClassMeans
p-4padding: 1rem — the scale is in units of 0.25rem, so 4 × 0.25 = 1rem
flexdisplay: flex
gap-3gap: 0.75rem
text-slate-500a specific grey from a designed palette
hover:bg-slate-50background on hover — a CSS state you cannot express in an inline style at all
md:flex-rowonly 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?

Install it — three steps

Read this before you google

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.

  1. Install, in the Vite project from Lesson 1.

    npm install tailwindcss @tailwindcss/vite
  2. 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()],
    })
  3. 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, ... */
}

The part everyone gets wrong

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.

Exercise

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.

Ask Martin. A good question to bring: "how does the team decide when a repeated set of classes becomes a component?" There is no rule in the docs — that one is team convention, and it is worth hearing his.