Getting Started

Tailwind CSS Fundamentals

Chapter 1 · Why Utility-First, and Getting Set Up

Tailwind CSS gives you a large set of small classes, each doing one job, and you style a page by putting them in your HTML: p-4 for padding, text-white for a white text colour, rounded-lg for rounded corners. A tool reads your files, finds the classes you used, and writes a CSS file containing only those. This chapter does the practical part first: build something, look at exactly what Tailwind produced, and learn how it decides what to produce. Everything after this chapter is learning which classes exist and how to combine them.

Every example here was run
The commands and output in this chapter come from running them with Tailwind CSS 4.3.3, Node.js 24.18, npm 11.16 and Vite 8.3 in a scratch project (the shell was Git Bash on Windows). Timings such as "Done in 50ms" vary from run to run, and file names such as Vite's index-DqPXTuJK.css change with the content. The Play CDN example was run in headless Chrome. Anything I did not run is labelled. This course uses Tailwind v4; the older v3 style (a tailwind.config.js file) is what CSS Frameworks 3 describes.

What a Utility Class Is

A utility class does one small thing, named after what it does. Here is a box with four of them:

<div class="p-4 bg-blue-500 text-white rounded-lg">A card</div>
.rounded-lg { border-radius: var(--radius-lg); } .bg-blue-500 { background-color: var(--color-blue-500); } .p-4 { padding: calc(var(--spacing) * 4); } .text-white { color: var(--color-white); }

The second box is the CSS Tailwind generated for those four classes (copied from the real output file, which you will meet in a moment). Each class is one rule, and each rule reads from a theme variable: --spacing is 0.25rem, so p-4 is four of those, 1rem. The names are short because you will type them hundreds of times: p is padding, bg is background, text is text colour (or size, depending on the value that follows), and the number is a step on a scale.

The argument for working this way, and the usual objection to it ("my markup is full of classes"), is made in CSS Frameworks 2, so it is not repeated here. In one sentence: you stop inventing names for things and stop switching between your HTML and a stylesheet, and in return you accept longer class attributes. The rest of this course shows how to keep that trade-off comfortable.

Your First Build with the CLI

The Tailwind CLI is the simplest way to see what Tailwind does, because nothing sits between you and the output. You need Node.js. In an empty folder:

npm init -y npm install tailwindcss @tailwindcss/cli mkdir src

Two files go in src. The CSS file is one line, and it is the whole of Tailwind's setup in v4:

/* src/input.css */ @import "tailwindcss";

The page uses the classes. It links output.css, which does not exist yet:

<!-- src/index.html --> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="./output.css" rel="stylesheet"> </head> <body> <h1 class="text-3xl font-bold underline">Hello world!</h1> <div class="p-4 bg-blue-500 text-white rounded-lg">A card</div> </body> </html>

Now build:

npx @tailwindcss/cli -i ./src/input.css -o ./src/output.css
≈ tailwindcss v4.3.3 Done in 50ms

Open src/index.html in a browser: a large bold underlined heading and a blue rounded box with white text. That is a working Tailwind project. The two flags are -i (input) and -o (output); add --minify for a smaller file when you publish.

What Is in the File Tailwind Wrote

Open output.css. For this page it is 5,676 bytes, or 5,009 bytes with --minify. It is organised into cascade layers, which you can see with one command:

grep "^@layer" src/output.css
@layer properties; @layer theme, base, components, utilities; @layer theme { @layer base { @layer utilities { @layer properties {

The second line declares the order: theme first, utilities last, and a later layer beats an earlier one no matter how specific the selectors are. That is why a utility such as p-4 reliably overrides the reset in the base layer. The layers hold:

  • theme: the variables your page uses, and only those.
  • base: Tailwind's reset (called Preflight), the same for every page.
  • utilities: one rule for each class Tailwind found in your files.
  • properties: a few internal variables (the --tw-… ones) that some utilities share.

The theme layer for this page is short, because the page uses few things:

sed -n '/^@layer theme {/,/^}/p' src/output.css
@layer theme { :root, :host { --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --color-blue-500: oklch(62.3% 0.214 259.815); --color-white: #fff; --spacing: 0.25rem; --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25 / 1.875); --font-weight-bold: 700; --radius-lg: 0.5rem; --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } }

Two things to notice. Tailwind has hundreds of colours, sizes and radii in its default theme, but the file contains only the ones this page needs: --color-blue-500 and --color-white, not the whole palette. And the colours are written in oklch(), a modern colour notation; browsers have only supported it for a few years, which is part of why Tailwind has browser requirements (see the end of this chapter). Chapter 9 shows how to add your own theme variables.

Preflight: why a bare heading looks like plain text
The base layer resets what browsers do by default. Among its rules, headings lose their size and weight, and lists lose their bullets:
grep -A3 -E "^ (h1, h2|ol, ul)" src/output.css
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } -- ol, ul, menu { list-style: none; } img, svg, video, canvas, audio, iframe, embed, object {

So an <h1> with no classes looks like body text, and a <ul> has no bullets: every visual decision is yours to make with a class. The same layer also zeroes margins and padding on everything, sets box-sizing: border-box, and makes images display: block with max-width: 100%. If a page looks "unstyled" after you add Tailwind, this layer is almost always the reason, and Exercise 3 below makes you read it.

How Tailwind Finds Your Classes

Tailwind does not run your code. It reads your project's files as plain text, looks for strings that match one of its class names, and generates a rule for each match. Two things follow. First, a class that is not in your files is not in the output: delete p-4 from the page, rebuild, and the .p-4 rule is gone (I checked: grep -c "\.p-4" printed 0). Second, the class has to appear in the file as a whole word. This is the mistake that catches everyone once:

// badge.js const color = "red"; el.className = `bg-${color}-500 p-2`; // pieced together at run time el.className2 = "bg-green-500 p-3"; // written out in full
.bg-green-500 .p-2 .p-3

Those three lines are the utilities Tailwind generated from that file. bg-green-500 is there because it is written out; bg-red-500 is not, because the text bg-red-500 never appears in the file, only the pieces bg- and -500. (p-2 is there because that part is written whole.) The fix is to write complete class names, even if it looks repetitive; Exercise 2 shows a tidy way to do it. Chapter 1 of Tailwind CSS Intermediate/Advanced covers the other options for classes Tailwind cannot see.

Which files does it read? By default it scans the project folder you run it from, and it skips some things. I tested this in a small git project with five files that each used a different margin class: src/a.html (m-1), ignored.html (m-9, listed in .gitignore), node_modules/pkg/index.html (m-8), notes.css (m-7) and pic.png (m-6, really a text file with a picture's extension). Only one class made it into the output:

grep -E "^\s+\.m-[0-9]" out.css
.m-1 {

Files in .gitignore, anything in node_modules, stylesheets and binary-looking files (images) are skipped. When I removed the .gitignore line and rebuilt, .m-9 appeared. Keep this in mind when a class works in one place and not another: the file may be ignored.

Watch Mode

Running the build by hand after every change gets tedious. Add --watch and the CLI stays running and rebuilds when a file changes:

npx @tailwindcss/cli -i ./src/input.css -o ./src/output.css --watch
When --watch exits straight away
In a normal terminal that command keeps running. But when I ran it from a script (so the program's standard input was not a terminal), it printed the banner, built nothing, and exited. The CLI's own help explains why: -w, --watch[=always], "use always to keep watching when stdin is closed". With --watch=always it built the file, then rebuilt each time I edited the page (the log showed a "Done" line for each build). If a watch task in your editor, a task runner or a container seems to do nothing, try --watch=always.

Using Tailwind with Vite

Most projects use a bundler, and Tailwind has a plugin for Vite. The Tailwind installation docs list Laravel, SvelteKit, React Router, Nuxt and SolidJS as frameworks that use this route; it is a good default whenever your project already runs on Vite. Install the plugin, add it to the Vite config, and import Tailwind in your CSS. No PostCSS configuration is needed:

npm install tailwindcss @tailwindcss/vite vite // vite.config.js import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [tailwindcss()], }) /* src/style.css */ @import "tailwindcss";

The page links the stylesheet (<link href="/src/style.css" rel="stylesheet">), and a normal Vite build does the rest. For the same one-heading page:

npx vite build
transforming... ✓ 3 modules transformed. rendering chunks... computing gzip size... dist/index.html 0.30 kB │ gzip: 0.23 kB dist/assets/index-DqPXTuJK.css 4.77 kB │ gzip: 1.63 kB ✓ built in 226ms

The CSS comes out at 4.77 kB (1.63 kB compressed) and is already minified, with the same layers as before. Notice that a framework with its own build, such as Astro or Next.js, needs its own variation of these steps; those are covered in Tailwind CSS Intermediate/Advanced 5. I did not run the Vite dev server in this chapter; the build above is what I ran.

The Play CDN: Prototypes Only

For a five-minute experiment you can skip the build entirely. One script tag loads Tailwind into the page, and it generates the CSS in the browser as you go:

<!doctype html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> </head> <body> <h1 class="text-3xl font-bold underline">Hello world!</h1> </body> </html>
with the script: font-size 30px | weight 700 | text-decoration underline without the script: font-size 32px | weight 700 | text-decoration none

The second box is what I measured in headless Chrome with and without the script: with it, text-3xl gave 30px (1.875rem) and underline took effect; without it, the browser's default heading showed. The Tailwind documentation says the Play CDN "is designed for development purposes only, and is not intended for production", because the browser does the work on every page view and downloads the script each time. Use it to try a class; use the CLI or Vite for anything you will publish. It also needs a network connection, which is a reason not to depend on it in a course exercise, so the exercises here use the CLI.

Which Way Should I Use?

RouteWhat it isUse it forRun here?
CLIOne command: input CSS in, output CSS out; --watch, --minifyPlain HTML sites, learning, small projects, back-end templatesYes
Vite plugin@tailwindcss/vite in vite.config; build and dev server handle the restAny project that already uses Vite (many frameworks do)Build: yes
Framework setupsPostCSS plugin or webpack plugin for tools that are not Vite-based (Next.js, for example)Whatever your framework's build usesNo (Tailwind CSS Intermediate/Advanced 5)
Play CDNOne <script> tag; CSS generated in the browserQuick experiments and demos onlyYes (headless Chrome)

Editor Setup

Two tools are worth installing before you write much markup. The Tailwind CSS IntelliSense extension for VS Code (extension ID bradlc.vscode-tailwindcss) gives class-name completion, shows the CSS a class produces when you hover it, and warns about conflicting classes. The Prettier plugin for Tailwind (npm package prettier-plugin-tailwindcss) sorts the classes in each class attribute into a consistent order, so that two people writing the same element produce the same text. Both are described, with their settings, in the VS Code Tailwind CSS Cheat Sheet in the Sidebar (Programming). I did not install them for this chapter.

Browser Support

Tailwind v4 is built for current browsers. The official compatibility page lists the versions the core framework depends on: Chrome 111 (March 2023), Safari 16.4 (March 2023) and Firefox 128 (July 2024). You have already seen why in the output file: it uses cascade layers (@layer), registered custom properties (@property) and oklch() colours. A few utilities go further and use very new CSS, such as field-sizing: content; the same page says to use those only if your audience's browsers support them. If you must support older browsers than these, v4 is not the right tool, and the v3 line is.

One last difference from older tutorials, since you will meet them: in v3 the CSS file began with three lines (@tailwind base;, @tailwind components;, @tailwind utilities;) and a tailwind.config.js file held the settings. In v4 it is the single @import "tailwindcss"; line you used above, and the settings live in the CSS itself. That is the subject of chapter 9.

Hands-On Exercises

Exercise 1

In an empty folder, set up the Tailwind CLI from scratch and build a page with a heading (text-2xl font-semibold) and a link styled as a button (inline-block rounded-md bg-teal-600 px-4 py-2 text-white hover:bg-teal-700). From the output file alone, prove three things: the rule for bg-teal-600 exists, the hover rule is wrapped in @media (hover: hover), and the two teal colours appear as variables in the theme layer.

📄 View solution
Exercise 2

A script builds badge classes with `bg-${color}-500 text-white px-2` for red, green and blue. Build it and show that no background colour is generated, then change the script (without changing what it displays) so that all three are.

📄 View solution
Exercise 3

Build a page with an <h1>, a two-item <ul> and an <img> and no classes at all. Find the three Preflight rules in the output that explain how each element will look. Then add classes to the list so it has bullets and indentation again, and report which utility rules Tailwind generated.

📄 View solution

Chapter 1 Quick Reference

  • A utility class does one small job (p-4, text-white, rounded-lg); Tailwind writes a CSS rule for each class it finds in your files, and only those
  • v4 setup is one CSS line: @import "tailwindcss"; (v3 used three @tailwind lines and a JS config file)
  • CLI: npm install tailwindcss @tailwindcss/cli, then npx @tailwindcss/cli -i ./src/input.css -o ./src/output.css; --minify for production; --watch to rebuild on change, and --watch=always when standard input is closed
  • Vite: @tailwindcss/vite in the plugins array, @import "tailwindcss"; in your CSS
  • Play CDN (@tailwindcss/browser@4): prototypes only, never production
  • The output has layers in the order theme, base, components, utilities; theme holds only the variables you use; base is Preflight, the reset
  • Tailwind reads files as text: write class names in full (bg-red-500, never bg-${color}-500); it skips .gitignored files, node_modules, stylesheets and images
  • Needs Chrome 111, Safari 16.4 or Firefox 128 or newer
  • Editor: bradlc.vscode-tailwindcss (IntelliSense) and prettier-plugin-tailwindcss (class sorting)
Coming next
Tailwind CSS Fundamentals 2 starts on the vocabulary: the spacing scale behind p-4, width and height, and the typography utilities for size, weight, line height and spacing.