Style web pages with small utility classes, straight in your HTML.
Tailwind CSS 4 from the very beginning: what HTML and CSS are, then utility classes for spacing, type, colour and layout, responsive design, states, dark mode and motion, all the way to themes with @theme, utilities and variants of your own, and how the engine builds your CSS. No experience needed: every idea lands on one running example, a small coffee shop’s website.
What you’ll be able to do
Explain how a browser applies CSS: selectors, the box model, the cascade and specificity.
Style any page with utilities and variants, mobile-first, with flexbox, grid and container queries.
Build accessible forms, states, dark mode and motion that respect people’s preferences.
Design a theme with @theme tokens, and extend Tailwind with @utility, @custom-variant and @source.
Predict which class wins, spot classes that produce no CSS, and review Tailwind code for a design system.
Modules
What a web page is made of· 8 slides
How CSS decides what things look like· 9 slides
Boxes, units and colours· 8 slides
Why utility classes· 8 slides
Installing Tailwind and your first styled page· 9 slides
Spacing and sizing· 8 slides
Typography· 9 slides
Colour, borders, corners and shadows· 10 slides
Layout with flexbox· 9 slides
Grid and positioning· 8 slides
Responsive design and container queries· 9 slides
States: hover, focus and friends· 10 slides
Dark mode· 8 slides
Forms and accessibility· 10 slides
Transitions, transforms and animation· 9 slides
Theming with @theme and design tokens· 9 slides
Components, reuse and @apply· 10 slides
Your own utilities, variants and sources· 8 slides
How the engine works, and Tailwind at scale· 8 slides
Mastery challenge· 11 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: What a web page is made of
Tailwind CSS1 / 178
Module 1 · What a web page is made of
A web page is text that a browser reads
Every page you visit starts as plain text files. A browser (Chrome, Firefox, Safari) downloads them and draws the page. Three languages share the work, and this course is about the second one.
HTMLWhat is on the page: headings, text, pictures, buttons
CSSHow it looks: colours, sizes, spacing, layout
JavaScriptWhat it does: reacting to clicks, loading data
Each language has one job.
Picture a small coffee shop’s website. HTML says “here is a menu with a latte at 3.50”. CSS says “make the menu a cream-coloured card with rounded corners”. Tailwind CSS, the tool this course teaches, is a faster way to write that CSS.
Browser
The program that downloads a page’s files and draws the page.
HTML
HyperText Markup Language: describes the content and structure of a page.
CSS
Cascading Style Sheets: describes how that content looks.
Tailwind CSS2 / 178
Module 1 · What a web page is made of
HTML is made of elements
An element is one piece of content wrapped in tags. The opening tag <h2> says where it starts, the closing tag </h2> (with a slash) says where it ends, and the content sits between.
WalkthroughInteractive · disabled in the PDF
menu.html
1<section>2<h2>Our menu</h2>3<p>Fresh coffee, roasted down the road.</p>4<ul>5<li>Espresso: 2.50</li>6<li>Latte: 3.50</li>7</ul>8<imgsrc="latte.jpg"alt="A latte with a leaf in the foam">9</section>
Lines 1,9:<section> groups related content. Everything inside belongs to it.
Lines 2:<h2> is a heading, second level. <h1> is the page’s main title.
Lines 3:<p> is a paragraph of text.
Lines 4-7:<ul> is a list with bullets, and each <li> is one item in it.
Lines 8:<img> shows a picture. It has no content, so it has no closing tag. alt describes the picture for people who can’t see it.
Fill the blanksInteractive · disabled in the PDF
Close the paragraph so the browser knows where it ends.
HTML
1<p>Open every day from 7am
Tailwind CSS3 / 178
Module 1 · What a web page is made of
Attributes add details, and class is the one CSS uses most
An attribute is extra information inside an opening tag, written name="value". src tells an image which file to show. href tells a link where to go. And class gives an element one or more labels, separated by spaces.
HTML
1<ahref="/menu"class="button big">See the menu</a>2<pclass="note">Oat milk is free.</p>
The link has two classes, button and big. The paragraph has one, note.
A class does nothing by itself. It is a hook: CSS can say “everything with the class note is grey”. Tailwind is built entirely on classes, so you will write a lot of them.
Predict the outputInteractive · disabled in the PDF
How many classes does this button have?
HTML
1<buttonclass="order now wide">Order</button>
How many classes does this button have?
Tailwind CSS4 / 178
Module 1 · What a web page is made of
Elements nest inside each other, like a family tree
When one element sits inside another, the outer one is the parent and the inner one is the child. Elements side by side with the same parent are siblings. The whole page becomes a tree, which the browser calls the DOM (Document Object Model).
section
h2Our menu
p
ul
liEspresso
liLatte
img
The menu from before, as the browser sees it.
Why the tree matters. Styles often depend on family: “a list item inside the menu”, or “when the card is hovered, change the title inside it”. You will meet both in CSS and in Tailwind.
QuizInteractive · disabled in the PDF
In the tree above, which elements are siblings of the ul?
In the tree above, which elements are siblings of the ul?
Tailwind CSS5 / 178
Module 1 · What a web page is made of
How a browser goes from files to pixels
The browser reads the HTML and builds the tree of elements.
It reads every stylesheet the page links to.
For each element it works out which CSS rules apply.
It calculates how big each element is and where it goes.
<head> holds information about the page. <body> holds what you see.
PlaygroundInteractive · disabled in the PDF
HTML
1<style>2h1{color:saddlebrown;}3.note{color:gray;font-style:italic;}4</style>56<h1>Corner Coffee</h1>7<p>Open every day from 7am.</p>8<pclass="note">Oat milk is free.</p>9<pstyle="color: seagreen">Ask about our beans.</p>
Change saddlebrown to teal, add class="note" to the first paragraph, or change the colour in the style attribute.
QuizInteractive · disabled in the PDF
The shop has ten pages and wants the same brown headings on all of them. Where should that CSS go?
The shop has ten pages and wants the same brown headings on all of them. Where should that CSS go?
Tailwind CSS7 / 178
Module 1 · What a web page is made of
Developer tools let you look inside any page
Every browser has developer tools (devtools). Right-click anything on a page and choose Inspect: a panel opens with the element’s HTML on one side and the CSS that applies to it on the other. You can edit both live, and nothing is saved, so it is safe to experiment.
The most useful habit in this course. When a style doesn’t do what you expect, inspect the element. The styles panel shows which rules matched, and crosses out the ones that lost. Most puzzles are solved there in seconds.
Try itInteractive · disabled in the PDF
Tailwind CSS8 / 178
Module 1 · What a web page is made of
Recap
HTML describes content as elements: an opening tag, content, a closing tag.
Attributes add details. class gives an element labels that CSS can target.
Elements nest into a tree: parents, children, siblings.
The browser builds the tree, matches CSS to it, lays it out, then paints.
CSS usually lives in one stylesheet file that every page links to.
Devtools show you the HTML and the CSS of any element.
FlashcardsInteractive · disabled in the PDF
Element
One piece of HTML: tags around some content, like <p>Hello</p>.
Tag
The <name> that opens an element, or </name> that closes it.
Attribute
Extra information in an opening tag: name="value".
Class
A label on an element, set with the class attribute, that CSS can target.
DOM
The tree of elements the browser builds from the HTML.
Stylesheet
A file of CSS, linked from the page’s <head>.
Devtools
The browser’s built-in panel for inspecting a page’s HTML and CSS.
Module 2: How CSS decides what things look like
Tailwind CSS9 / 178
Module 2 · How CSS decides what things look like
A CSS rule is a selector and some declarations
WalkthroughInteractive · disabled in the PDF
style.css
1h1{2color:brown;3font-size:32px;4}
Lines 1: The selector, h1, picks which elements the rule applies to: every <h1>.
Lines 1,4: Curly braces hold the rule’s declarations.
Lines 2: A declaration is a property and a value: color is the property, brown is the value. A semicolon ends it.
Lines 3: A rule can have as many declarations as you like, one per line.
Rule
A selector followed by declarations in braces.
Selector
The part that chooses which elements a rule styles.
Property
What to change: color, padding, font-size.
Value
What to change it to: brown, 16px, bold.
Tailwind CSS10 / 178
Module 2 · How CSS decides what things look like
Selectors pick elements by tag, class or position
Selector
Picks
Example
p
Every element of that tag
All paragraphs
.price
Every element with that class (note the dot)
<span class="price">
#logo
The one element with that id
<img id="logo">
.menu li
An li anywhere inside .menu (a space means “inside”)
Menu items
.menu > li
An li that is a direct child of .menu
Only the top level
h1, h2
Both (a comma means “and also”)
All big headings
QuizInteractive · disabled in the PDF
Which selector styles only the prices, written as <span class="price">3.50</span>?
Which selector styles only the prices, written as <span class="price">3.50</span>?
Tailwind CSS11 / 178
Module 2 · How CSS decides what things look like
Many rules can style the same element
The browser collects every rule whose selector matches an element and applies all their declarations together. Rules don’t replace each other unless they set the same property.
Note. This is how Tailwind works too: an element with five classes gets five small rules, each setting one or two properties, and they all add up.
Tailwind CSS12 / 178
Module 2 · How CSS decides what things look like
When rules disagree, the cascade picks a winner
If two matching rules set the same property, only one value can win. The browser decides with the cascade, a fixed list of tie-breakers. The two you meet every day are specificity (how precise the selector is) and order (if precision ties, the rule written later wins).
Predict the outputInteractive · disabled in the PDF
What colour is <p class="note">?
CSS
1.note{2color:green;3}4.note{5color:orange;6}
What colour is <p class="note">?
Tailwind CSS13 / 178
Module 2 · How CSS decides what things look like
More precise selectors beat less precise ones
Specificity is counted in three columns: ids, then classes (and attributes and pseudo-classes), then tags. Compare the first column; only if it ties, compare the next. An id beats any number of classes, and a class beats any number of tags.
Selector
Ids
Classes
Tags
p
0
0
1
.note
0
1
0
p.note
0
1
1
#intro
1
0
0
PlaygroundInteractive · disabled in the PDF
HTML
1<style>2.note{color:green;}3p{color:orange;}4p.note{color:purple;}5</style>67<p>A plain paragraph.</p>8<pclass="note">A paragraph with the class note.</p>
The second paragraph is purple: p.note is the most specific. Delete that rule and it turns green, not orange: p comes later, but a class beats a tag, so order loses to specificity.
Predict the outputInteractive · disabled in the PDF
What colour is <p class="note">?
CSS
1.note{2color:green;3}4p{5color:orange;6}
What colour is <p class="note">?
Going deeper
Inline style attributes beat any selector, and !important on a declaration beats normal declarations. Both are escape hatches that make styles hard to override later, so use them rarely. Cascade layers, which Tailwind relies on, are another tie-breaker that comes before specificity: you will meet them in the expert modules.
Tailwind CSS14 / 178
Module 2 · How CSS decides what things look like
Some properties pass from parent to child
Set color or font-family on a parent and its children use it too, unless something sets their own. That is inheritance. Properties about text usually inherit. Properties about the box itself (padding, border, background) don’t.
CSS
1.menu{2color:brown;/* the li text inside turns brown */3padding:16px;/* only .menu gets padding */4}
QuizInteractive · disabled in the PDF
Which of these does a child inherit from its parent by default?
Which of these does a child inherit from its parent by default?
Tailwind CSS15 / 178
Module 2 · How CSS decides what things look like
Pseudo-classes style an element in a state
A pseudo-class starts with a colon and matches an element only in a certain situation: :hover while the pointer is over it, :focus while it has the keyboard’s attention, :first-child if it comes first among its siblings. A pseudo-element (two colons) styles a part of an element, such as ::placeholder in an input.
Fill the blanksInteractive · disabled in the PDF
Make links turn orange while the pointer is over them.
CSS
1a{2:orange;3}
Tailwind CSS16 / 178
Module 2 · How CSS decides what things look like
Media queries apply rules only on some screens
A media query wraps rules in a condition about the device. The most common asks how wide the screen is. Inside it, rules apply only while the condition is true, and the browser re-checks as the window resizes.
CSS
1.menu{2font-size:16px;3}4@media(min-width:768px){5.menu{6font-size:20px;/* only on screens 768px or wider */7}8}
QuizInteractive · disabled in the PDF
On a phone 390px wide, what is the menu’s font size?
On a phone 390px wide, what is the menu’s font size?
Tailwind CSS17 / 178
Module 2 · How CSS decides what things look like
Recap
A rule is a selector plus declarations; a declaration is a property and a value.
Selectors pick elements by tag (p), class (.note), id (#logo) or position (.menu li).
Every matching rule applies; they only compete for the same property.
The cascade picks the winner: specificity first, then the later rule.
Text properties inherit from parents; box properties don’t.
Pseudo-classes (:hover) and media queries (@media) apply rules conditionally.
FlashcardsInteractive · disabled in the PDF
Declaration
A property and its value, like color: brown;.
Cascade
The browser’s rules for choosing a winner when declarations conflict.
Specificity
How precise a selector is: ids, then classes, then tags.
Inheritance
A child using its parent’s value for properties like color.
Pseudo-class
A selector part like :hover that matches an element in a state.
Media query
@media (…): rules that apply only when a condition about the device is true.
Module 3: Boxes, units and colours
Tailwind CSS18 / 178
Module 3 · Boxes, units and colours
Every element is a box with four layers
The browser draws every element as a rectangle. From the inside out, the box model has four layers. Getting comfortable with them is most of what “spacing” means in CSS and in Tailwind.
MarginEmpty space outside the border, pushing other boxes away
BorderA line around the padding (can be zero wide)
PaddingSpace inside the border, around the content; shows the background
ContentThe text or image itself
Outside to inside.
CSS
1.menu-card{2padding:16px;/* breathing room inside */3border:1pxsolidtan;/* a thin line */4margin:24px;/* distance from neighbours */5}
Change padding to 40px and the white grows around the text. Change margin to 0 and the cards touch. Try border: 8px solid tan.
QuizInteractive · disabled in the PDF
The menu card’s text touches its border. Which property gives the text room without moving the card?
The menu card’s text touches its border. Which property gives the text room without moving the card?
Tailwind CSS19 / 178
Module 3 · Boxes, units and colours
box-sizing decides what width measures
By default, width sets only the content. Padding and border are added on top, so a width: 200px box with 16px padding and a 1px border is really 234px wide. box-sizing: border-box makes width include padding and border, which is far easier to reason about. Tailwind turns it on for every element.
Both say width: 200px, but the first is wider. Raise padding to 40px: the first box grows, the second keeps its width.
QuizInteractive · disabled in the PDF
With border-box, a box has width: 300px, padding: 20px and a 5px border. How wide is its content area?
With border-box, a box has width: 300px, padding: 20px and a 5px border. How wide is its content area?
Tailwind CSS20 / 178
Module 3 · Boxes, units and colours
Block boxes stack, inline boxes flow with text
display
Behaves like
Default for
block
Takes the full width; the next box starts on a new line
div, p, h1, section, ul
inline
Sits inside a line of text; ignores width, height and top and bottom margin
span, a, strong
inline-block
Sits in a line, but respects width, height and all spacing
button, input, select
none
Not drawn at all, and takes no space
Hidden things
Two more values, flex and grid, turn a box into a layout container for its children. They get whole modules later.
QuizInteractive · disabled in the PDF
You set width: 120px on a <a> link, and nothing changes. Why?
You set width: 120px on a <a> link, and nothing changes. Why?
Tailwind CSS21 / 178
Module 3 · Boxes, units and colours
Vertical margins between blocks can merge
When two block boxes sit one above the other, the bottom margin of the first and the top margin of the second don’t add up: the bigger one wins. This is margin collapsing. It surprises everyone once.
QuizInteractive · disabled in the PDF
A heading has margin-bottom: 24px and the paragraph below has margin-top: 16px. How big is the gap?
A heading has margin-bottom: 24px and the paragraph below has margin-top: 16px. How big is the gap?
Tailwind CSS22 / 178
Module 3 · Boxes, units and colours
Sizes have units: px, rem, em, % and viewport units
Unit
Means
Good for
px
A CSS pixel, a fixed size
Borders, small details
rem
Times the root font size (usually 16px)
Spacing and text: Tailwind’s main unit
em
Times the current element’s font size
Spacing that grows with its own text
%
A share of the parent’s size
Widths inside a container
vw / vh
1% of the window’s width / height
Full-screen sections
dvh
1% of the visible height, adjusting for phone toolbars
Full-height layouts on phones
rem stands for “root em”: it is relative to the font size of the <html> element. If someone raises their browser’s default text size to 20px, every rem grows with it, so the page scales for people who need larger text. A pixel value would ignore their choice.
QuizInteractive · disabled in the PDF
With the usual 16px root font size, how much is 1.5rem?
With the usual 16px root font size, how much is 1.5rem?
Tailwind CSS23 / 178
Module 3 · Boxes, units and colours
Colours can be written several ways
Format
Example
Reads as
Name
brown
One of about 150 named colours
Hex
#6f4e37
Red, green, blue as two hex digits each
rgb()
rgb(111 78 55)
Red, green, blue from 0 to 255
hsl()
hsl(25 34% 33%)
Hue (an angle), saturation, lightness
oklch()
oklch(0.45 0.06 56)
Lightness, chroma (colourfulness), hue
Add a slash and an alpha to make a colour see-through: rgb(0 0 0 / 0.5) is half-transparent black. Tailwind’s built-in colours are written in oklch, a format where equal steps in lightness look equal to the eye, which makes palettes more even.
Fill the blanksInteractive · disabled in the PDF
Make a black that is 20% opaque.
CSS
1background-color: rgb(0 0 0 );
Tailwind CSS24 / 178
Module 3 · Boxes, units and colours
CSS variables store a value under a name
A custom property, usually called a CSS variable, is a property whose name starts with two dashes. You set it once and read it anywhere with var(). Children inherit it, and a rule lower down can change it. Tailwind 4 is built on them: every colour, size and font it knows is a variable like this.
:root matches the <html> element, so the variable is available everywhere.
QuizInteractive · disabled in the PDF
The shop rebrands to a darker brown. With the CSS above, what do you change?
The shop rebrands to a darker brown. With the CSS above, what do you change?
Tailwind CSS25 / 178
Module 3 · Boxes, units and colours
Recap
Every element is a box: content, padding, border, margin.
border-box makes width include padding and border; Tailwind sets it everywhere.
Block boxes stack and fill the width; inline boxes flow with text and ignore width.
Vertical margins between blocks collapse to the larger one.
rem scales with the reader’s text size; it is Tailwind’s main unit.
Colours take an optional alpha after a slash. Tailwind’s palette uses oklch.
CSS variables (--name, read with var()) keep a value in one place.
FlashcardsInteractive · disabled in the PDF
Box model
Content, padding, border and margin, from inside to outside.
Padding vs margin
Padding is inside the border and shows the background; margin is outside.
box-sizing: border-box
width and height include padding and border.
rem
A multiple of the root font size, 16px by default.
Alpha
How opaque a colour is, from 0 (clear) to 1 (solid).
Custom property
A CSS variable: --name: value, read with var(--name).
Design token
A named design decision, like a brand colour, kept in one place.
Module 4: Why utility classes
Tailwind CSS26 / 178
Module 4 · Why utility classes
The usual way: name a thing, then style the name
With plain CSS you invent a class for each thing (menu-card), then write a rule for it in a stylesheet. It works, but as a site grows the stylesheet grows with it, and three problems appear.
Naming: every new box needs a name, and good names are hard (card-inner-wrapper-2).
Two places: to change how something looks you jump between the HTML and the CSS.
Fear of deleting: nobody knows which pages still use .menu-card, so old CSS piles up.
Tailwind CSS27 / 178
Module 4 · Why utility classes
Utility classes each do one small thing
A utility class sets one property (or a few that belong together) to one value. p-6 sets padding. rounded-lg rounds the corners. font-bold makes text bold. You style an element by listing utilities in its class attribute, so the look is written right where the element is.
Nothing to name, nothing to keep in sync, and deleting the HTML deletes its styles. This approach is called utility-first, and Tailwind CSS is the most widely used tool for it.
PlaygroundInteractive · disabled in the PDF
HTML
1<divclass="p-6 rounded-lg bg-white shadow">2<h2class="font-bold">Latte</h2>3<pclass="text-stone-600">Espresso and steamed milk.</p>4</div>
Change p-6 to p-12, rounded-lg to rounded-full, or add text-2xl to the heading. Each class changes one thing.
Tailwind CSS28 / 178
Module 4 · Why utility classes
A class name reads as property, then value
Class
Reads as
CSS it writes
p-4
padding, size 4
padding: calc(var(--spacing) * 4), which is 1rem
mt-2
margin-top, size 2
margin-top: calc(var(--spacing) * 2)
text-lg
text, large
font-size: 1.125rem (and a line height)
bg-amber-100
background, amber, shade 100
background-color: var(--color-amber-100)
rounded-lg
rounded corners, large
border-radius: var(--radius-lg)
hover:bg-amber-200
when hovered: background amber 200
The same, inside a :hover rule
The part before a colon, like hover:, is a variant: it says when the utility applies. You will meet many variants; they all work with all utilities.
QuizInteractive · disabled in the PDF
What does px-3 most likely do?
What does px-3 most likely do?
Tailwind CSS29 / 178
Module 4 · Why utility classes
Tailwind reads your files and writes only the CSS you use
Tailwind doesn’t ship a giant stylesheet of every possible class. It scans your project’s files for words that look like class names, and writes CSS for exactly those. Use p-4 and bg-amber-100, and your stylesheet contains those two rules and nothing else of that kind.
A consequence to remember. Tailwind reads your files as plain text. It never runs your code. A class name it can’t see written out in full gets no CSS. This one rule explains many “my class does nothing” puzzles later.
QuizInteractive · disabled in the PDF
Your site never uses the class bg-pink-500. Is it in your final CSS?
Your site never uses the class bg-pink-500. Is it in your final CSS?
Tailwind CSS30 / 178
Module 4 · Why utility classes
Utilities are not the same as inline styles
Utilities live in the HTML, like a style attribute, but they can do what style can’t, and they keep a site consistent.
`style` attribute
Any value at all: 13px here, 15px there
No hover, focus or other states
No different styles for small screens
Hard to override, because it beats every selector
Utility classes
Values come from a shared scale, so things line up
hover:, focus: and friends
md:, lg: for bigger screens
Plain one-class rules, easy to override
QuizInteractive · disabled in the PDF
Which of these can a style attribute not do?
Which of these can a style attribute not do?
Tailwind CSS31 / 178
Module 4 · Why utility classes
“But the HTML gets long”
It does, and that is the most common complaint. In practice it matters less than it seems:
You read classes left to right like a description: padding, corners, background.
Repeated markup is usually made by a loop or a component, so the classes are written once.
Editor extensions complete class names and show the CSS behind each one.
When you delete an element, its styles go with it: nothing is left behind.
Note. Tailwind still lets you write normal CSS whenever it is the better tool. Later modules show when to extract a component, and when @apply or a custom utility makes sense.
Tailwind CSS32 / 178
Module 4 · Why utility classes
Practice: from CSS to utilities
Here is a rule from the coffee shop’s old stylesheet. Rewrite it as utilities. A hint: 1rem is spacing step 4, and font-weight: 700 is font-bold.
Custom class names bring naming, two-file editing and CSS nobody dares delete.
A utility class does one small thing; you combine them in class.
Names read as property then value: p-4, text-lg, bg-amber-100.
A prefix with a colon is a variant: hover:, md:.
Tailwind scans your files as text and writes CSS only for classes it finds.
Unlike style, utilities use a shared scale and support states and screen sizes.
FlashcardsInteractive · disabled in the PDF
Utility class
A class that sets one small piece of style, like p-4 or font-bold.
Utility-first
Styling by combining utilities in the HTML rather than naming every part.
Variant
A prefix like hover: or md: that says when a utility applies.
Scanning
Tailwind reading your files as text to find the classes you use.
Why a class gets no CSS
Tailwind never saw it written out in full in a file it scans.
Module 5: Installing Tailwind and your first styled page
Tailwind CSS34 / 178
Module 5 · Installing Tailwind and your first styled page
Three tools before you start
Terminal
A window where you type commands instead of clicking. Terminal on macOS and Linux, PowerShell or Terminal on Windows.
Node.js
A program that runs JavaScript outside the browser. Tailwind’s tools run on it. Install the LTS version from nodejs.org.
npm
Node’s package manager, installed with it. A package is code someone else published; npm install downloads it into your project.
Code editor
Where you write files. VS Code is free and has an official Tailwind extension.
Terminal
1node--version2npm--version
Type these in a terminal. If each prints a version number, you are ready.
Tailwind CSS35 / 178
Module 5 · Installing Tailwind and your first styled page
Make a project with Vite
Vite is a small tool that serves your files to the browser while you work, and refreshes the page when you save. It is the quickest way to try Tailwind. These commands make a folder called coffee-shop with a plain HTML and JavaScript starter.
cd means “change directory”: move into the new folder. If the first command asks whether to install and start now, answer No: the next two commands do that.
coffee-shop/
index.html# the page
package.json# the project's name and its packages
src/
main.js# JavaScript the page loads
style.css# the stylesheet main.js imports
The parts you will touch. Vite adds a few more files.
QuizInteractive · disabled in the PDF
What does the last command, npm install, do?
What does the last command, npm install, do?
Tailwind CSS36 / 178
Module 5 · Installing Tailwind and your first styled page
Install Tailwind and plug it into Vite
Terminal
1npminstalltailwindcss@tailwindcss/vite
Two packages: Tailwind itself, and the piece that connects it to Vite.
Lines 1-2: Load Vite’s config helper and Tailwind’s Vite plugin. Create this file next to package.json.
Lines 4-6: A plugin adds a feature to a tool. This one makes Vite pass every CSS file through Tailwind.
Tailwind CSS37 / 178
Module 5 · Installing Tailwind and your first styled page
One line of CSS switches it on
Replace everything in src/style.css with a single line. It pulls in Tailwind’s theme (its colours, sizes and fonts), its base styles and its utilities. Tailwind 4 needs no other configuration file to start.
src/style.css
1@import"tailwindcss";
If you have seen older tutorials. Tailwind 3 used three @tailwind lines and a tailwind.config.js file. Tailwind 4 replaced them with this one @import, and you configure it in CSS instead. Older guides still work as background, but their setup steps don’t apply.
Fill the blanksInteractive · disabled in the PDF
Write the line that loads Tailwind into a stylesheet.
CSS
1"";
Tailwind CSS38 / 178
Module 5 · Installing Tailwind and your first styled page
Replace the <body> Vite made. The script line loads main.js, which loads the CSS. Cut src/main.js down to its first line, import "./style.css": the rest of it fills in the demo page you just replaced, and fails without it.
PlaygroundInteractive · disabled in the PDF
HTML
1<divclass="bg-amber-50 p-8">2<h1class="text-4xl font-bold text-amber-900">3 Corner Coffee4</h1>5<pclass="mt-2 text-stone-600">Small batch, big flavour.</p>6</div>
The same page, styled live. Change text-4xl to text-6xl, or bg-amber-50 to bg-sky-50, before you set up your own project.
Terminal
1npmrundev
Starts Vite. Open the address it prints (usually http://localhost:5173). Stop it with Ctrl+C.
Try itInteractive · disabled in the PDF
Tailwind CSS39 / 178
Module 5 · Installing Tailwind and your first styled page
Preflight wipes the browser’s default look
Browsers give elements a default look: big bold headings, bullets on lists, margins on paragraphs. Each browser does it slightly differently. Tailwind starts with Preflight, a set of base styles that removes most of it, so a page looks the same everywhere and only shows what you asked for.
Margins on headings, paragraphs and lists are removed.
Headings are the same size and weight as normal text.
Lists have no bullets or numbers.
Images are display: block and never wider than their container.
Every element uses box-sizing: border-box.
QuizInteractive · disabled in the PDF
You write <h1>Menu</h1> with no classes in a Tailwind project. How does it look?
You write <h1>Menu</h1> with no classes in a Tailwind project. How does it look?
Tailwind CSS40 / 178
Module 5 · Installing Tailwind and your first styled page
Other ways to run Tailwind
Setup
Install
When
Vite plugin
@tailwindcss/vite
Most modern projects (and frameworks built on Vite)
PostCSS plugin
@tailwindcss/postcss
Tools that process CSS with PostCSS, such as Next.js
Command line
@tailwindcss/cli
No build tool at all: you run a command to write a CSS file
The CLI reads input.css, writes output.css, and keeps watching for changes.
Install the editor extension. The Tailwind CSS IntelliSense extension completes class names as you type, shows the CSS behind a class when you hover it, and warns about classes that conflict. It saves hours.
Tailwind CSS41 / 178
Module 5 · Installing Tailwind and your first styled page
When a class does nothing
Is the dev server running, and is the page the one it serves?
Does the stylesheet with @import "tailwindcss"; actually load? Check devtools for the CSS file.
Is the class spelled right? bg-amber-100, not bg-amber100 or background-amber-100.
Inspect the element. If no rule for the class exists, Tailwind never generated it.
If a rule exists but is crossed out, another rule won: read which one.
Find the bugInteractive · disabled in the PDF
The Mocha price should be bold and brown like the Latte one, but it isn’t bold. Which line is wrong?
HTML
Tailwind CSS42 / 178
Module 5 · Installing Tailwind and your first styled page
Recap
You need a terminal, Node.js with npm, and an editor.
With Vite: install tailwindcss and @tailwindcss/vite, then add the plugin.
@import "tailwindcss"; in your CSS is the whole setup in Tailwind 4.
Preflight removes browser defaults, so headings and lists start plain.
Unknown or misspelt classes produce no CSS and no error: inspect to check.
FlashcardsInteractive · disabled in the PDF
npm
Node’s package manager: npm install downloads packages into a project.
Vite
A development server and build tool; npm run dev starts it.
Plugin
A piece that adds a feature to a tool, like Tailwind to Vite.
@import "tailwindcss";
Loads Tailwind’s theme, base styles and utilities into a stylesheet.
Preflight
Tailwind’s base styles, which reset the browser’s default look.
A misspelt class
Produces no CSS and no error message.
Module 6: Spacing and sizing
Tailwind CSS43 / 178
Module 6 · Spacing and sizing
One number drives every space
Tailwind’s theme has one variable for spacing, --spacing, set to 0.25rem (4px at default text size). A spacing utility multiplies it: p-4 is 4 × 0.25rem = 1rem. Because every gap comes from the same step, things line up without you measuring.
Class
Value
At 16px root
p-1
0.25rem
4px
p-2
0.5rem
8px
p-4
1rem
16px
p-6
1.5rem
24px
p-8
2rem
32px
p-px
1px
1px
p-0
0
0
Any whole number works (p-13, p-30), and so do quarter steps such as p-1.5 or p-2.5. Tailwind 3 only had a fixed list; Tailwind 4 calculates the value from the number.
QuizInteractive · disabled in the PDF
How much padding is p-10?
How much padding is p-10?
Tailwind CSS44 / 178
Module 6 · Spacing and sizing
Padding: all sides, one axis, or one side
Prefix
Sides
Example
p-
All four
p-6
px-
Left and right (the x axis)
px-4
py-
Top and bottom (the y axis)
py-2
pt-pr-pb-pl-
Top, right, bottom, left
pt-8
ps-pe-
Start and end of the line (left and right in English)
ps-3
HTML
1<ahref="/order"class="px-5 py-2 bg-amber-700 text-white">2 Order now3</a>
Buttons usually have more room at the sides than above and below.
Start and end.ps- and pe- follow the reading direction. In a right-to-left language such as Arabic, ps-3 pads the right. Use them in sites that are translated into those languages.
PlaygroundInteractive · disabled in the PDF
HTML
1<ahref="#"class="inline-block px-5 py-2 bg-amber-700 text-white">2 Order now3</a>45<divclass="mt-6 p-4 bg-amber-100">6<pclass="bg-white">The padding is the amber around me.</p>7</div>
Change px-5 to px-10, then p-4 to p-8. Try pt-12 on the box: only the top grows.
QuizInteractive · disabled in the PDF
Which gives 1rem on the left and right, and 0.5rem on top and bottom?
Which gives 1rem on the left and right, and 0.5rem on top and bottom?
Tailwind CSS45 / 178
Module 6 · Spacing and sizing
Margin pushes things apart, and mx-auto centres
Margin utilities use the same prefixes with m: m-4, mt-2, mx-6. Two extras are worth learning now. A minus sign in front makes a negative margin, which pulls an element outwards (-mt-4). And mx-auto sets the left and right margins to auto, which centres a block that has a width.
HTML
1<mainclass="mx-auto max-w-3xl px-4">2<h1class="mt-12 text-3xl font-bold">Our menu</h1>3<pclass="mt-4">Everything is made fresh every morning.</p>4</main>
A page column: at most max-w-3xl wide, centred, with a little room at the sides on phones.
QuizInteractive · disabled in the PDF
You add mx-auto to a div and it doesn’t move. What is most likely missing?
You add mx-auto to a div and it doesn’t move. What is most likely missing?
Tailwind CSS46 / 178
Module 6 · Spacing and sizing
Space between children: gap and space-*
Most spacing is between siblings, such as menu items in a list. Two tools do it from the parent, so the children don’t each need a margin.
Change gap-3 to gap-8, or to gap-0. The space is only between items, never above the first.
Tailwind CSS47 / 178
Module 6 · Spacing and sizing
Widths and heights
Class
Sets
Use
w-64
width: 16rem (spacing scale)
A fixed width
w-1/2
width: 50%
A share of the parent
w-full
width: 100%
Fill the parent
w-screen
width: 100vw
The whole window
h-12
height: 3rem
A fixed height
min-h-dvh
At least the visible screen height
A full-height page
size-10
Width and height together
Icons, avatars
Prefer letting content set the height. A fixed h-40 on a box of text breaks as soon as the text is longer or the reader’s font is bigger: the text spills out. Let height come from the content and padding, and use min-h-* when you need a floor.
QuizInteractive · disabled in the PDF
Which makes a 3rem square for a coffee-cup icon?
Which makes a 3rem square for a coffee-cup icon?
Tailwind CSS48 / 178
Module 6 · Spacing and sizing
max-w-* keeps things readable
A max width stops a box from growing past a size but lets it shrink on small screens. That is what you want for page columns and paragraphs. max-w-* uses a separate scale of container sizes, from max-w-3xs (16rem) to max-w-7xl (80rem), plus max-w-prose, a width of about 65 characters that is comfortable to read.
Fill the blanksInteractive · disabled in the PDF
Make a column at most max-w-2xl wide, centred, with 1rem padding at the sides.
HTML
1<mainclass="">…</main>
Tailwind CSS49 / 178
Module 6 · Spacing and sizing
Square brackets for a value outside the scale
Sometimes you need an exact value: a logo that must be 137px wide. Put any CSS value in square brackets and Tailwind writes it as is: w-[137px]. Spaces aren’t allowed in a class, so write an underscore where the CSS needs a space. To read a CSS variable, use round brackets: p-(--card-padding).
Tip. Brackets are an escape hatch. If the same odd value shows up three times, it is a design decision: add it to the theme instead, so it gets a name.
Find the bugInteractive · disabled in the PDF
One of these classes produces no CSS. Which line?
HTML
Tailwind CSS50 / 178
Module 6 · Spacing and sizing
Recap
Spacing utilities multiply --spacing (0.25rem): p-4 is 1rem. Any whole or quarter number works.
p, px, py, pt…: all sides, an axis, one side. Margins use m the same way.
mx-auto centres a block that has a width; -mt-2 is a negative margin.
Space between children with gap on a flex or grid parent.
w-*, h-*, size-* set sizes; max-w-* caps them for readable columns.
[…] takes any value; (--var) reads a variable. Use them sparingly.
FlashcardsInteractive · disabled in the PDF
--spacing
The base spacing step, 0.25rem. Spacing utilities multiply it.
px-4 vs py-4
Left and right padding vs top and bottom padding, 1rem each.
mx-auto
Auto left and right margins: centres a block narrower than its parent.
gap-*
Space between the items of a flex or grid container.
size-*
Sets width and height at once.
Arbitrary value
A value in brackets, like w-[137px]. Underscores stand for spaces.
Module 7: Typography
Tailwind CSS51 / 178
Module 7 · Typography
Three font families to start with
A font family is a typeface, like Helvetica or Georgia. Tailwind’s theme has three: font-sans (no little feet on the letters, the default for the page), font-serif (with them, like a newspaper) and font-mono (every letter the same width, for code). Each is a list of fonts; the browser uses the first one the device has.
HTML
1<h1class="font-serif text-4xl">Corner Coffee</h1>2<pclass="font-sans">Open 7am to 6pm.</p>3<codeclass="font-mono">WIFI: ask at the counter</code>
Sans-serif
Letters without small strokes at their ends. Clean on screens.
Serif
Letters with small strokes at their ends. Classic, bookish.
Monospace
Every character the same width. Used for code.
Tailwind CSS52 / 178
Module 7 · Typography
Text sizes come with a matching line height
Class
Font size
Line height
text-xs
0.75rem (12px)
1rem
text-sm
0.875rem (14px)
1.25rem
text-base
1rem (16px)
1.5rem
text-lg
1.125rem (18px)
1.75rem
text-2xl
1.5rem (24px)
2rem
text-4xl
2.25rem (36px)
2.5rem
text-6xl
3.75rem (60px)
1 (as tall as the text)
Line height is the height of each line of text, which sets the space between lines. Big text needs relatively less of it. Each text-* size sets a sensible one for you; add a slash to choose another: text-lg/7 is size lg with a line height of 7 spacing steps (1.75rem).
PlaygroundInteractive · disabled in the PDF
HTML
1<articleclass="max-w-md">2<h2class="text-2xl font-bold">Our house blend</h2>3<pclass="text-base text-stone-700">4 Beans from two farms, roasted medium dark, with notes of5 chocolate and plum. Good as an espresso, better with milk.6</p>7</article>
Change text-2xl to text-4xl, then text-base to text-base/8 and watch the lines spread apart while the letters stay the same size.
QuizInteractive · disabled in the PDF
What does text-base/8 do?
What does text-base/8 do?
Tailwind CSS53 / 178
Module 7 · Typography
Weight, style and case
Class
CSS
font-normal
font-weight: 400
font-medium
font-weight: 500
font-semibold
font-weight: 600
font-bold
font-weight: 700
italic / not-italic
font-style: italic / normal
uppercase / lowercase / capitalize
text-transform
A font must have the weight. If the font you load only comes in 400 and 700, font-semibold makes the browser pick the nearest or fake it. Check which weights your font includes, and load only those you use.
Tip. Write the words in normal case in the HTML and add uppercase for the look. Screen readers may spell out text typed in capitals letter by letter, and copying it gives the capitals too.
Tailwind CSS54 / 178
Module 7 · Typography
Line height and letter spacing
Tailwind calls line height leading (the strips of lead printers put between lines) and letter spacing tracking. leading-* takes a spacing step (leading-6 is 1.5rem) or a name (leading-tight is 1.25, leading-relaxed 1.625). tracking-* uses names: tracking-tight, tracking-wide, tracking-widest.
HTML
1<h2class="text-5xl font-bold leading-tight tracking-tight">2 Slow coffee for fast mornings3</h2>4<pclass="text-xs uppercase tracking-widest text-stone-500">5 Since 20146</p>
Large headings look better tighter; small capitals need more room between letters.
Fill the blanksInteractive · disabled in the PDF
Give the paragraph a relaxed line height and slightly wide letter spacing.
HTML
1<pclass="">Our beans…</p>
Tailwind CSS55 / 178
Module 7 · Typography
Colour and alignment
Text colour uses text- plus a colour name and shade: text-stone-700. (The next module covers colours in full.) Alignment uses text-left, text-center, text-right and text-justify. text-start and text-end follow the reading direction.
HTML
1<articleclass="text-center">2<h3class="text-2xl font-semibold text-amber-900">Latte</h3>3<pclass="text-stone-600">Espresso and steamed milk.</p>4<pclass="mt-2 text-lg font-bold text-amber-700">3.50</p>5</article>
QuizInteractive · disabled in the PDF
text- starts both text-lg and text-amber-700. How does Tailwind tell them apart?
text- starts both text-lg and text-amber-700. How does Tailwind tell them apart?
Tailwind CSS56 / 178
Module 7 · Typography
Long text: truncating, clamping and wrapping well
Class
Does
truncate
One line, cut off with “…” when it is too long
line-clamp-3
At most three lines, then “…”
text-balance
Evens out line lengths: good for headings
text-pretty
Avoids a single word alone on the last line: good for paragraphs
wrap-break-word
Breaks a long word (like a web address) rather than overflowing
QuizInteractive · disabled in the PDF
Product cards in a grid show a description, and long ones push the cards out of line. Which keeps every description to two lines?
Product cards in a grid show a description, and long ones push the cards out of line. Which keeps every description to two lines?
Tailwind CSS57 / 178
Module 7 · Typography
Lists and links need their looks back
Preflight removes bullets from lists. Put them back with list-disc (bullets) or list-decimal (numbers), plus list-inside or some left padding so they have room. Links keep their colour from the surrounding text; add underline and choose the line’s look with decoration-* and underline-offset-*.
The steps for ordering ahead should be numbered, but no numbers appear. Which line is the problem?
HTML
Tailwind CSS58 / 178
Module 7 · Typography
Using your own font
To use a font such as one from Google Fonts, load it in the page’s <head>, then add it to the theme as a --font-* variable. Tailwind creates a utility from the name. The @theme block gets its own module later; for now, read it as “add these to Tailwind’s design settings”.
font-sans, font-serif, font-mono pick a family; font-bold and friends pick a weight.
text-* sizes include a line height; text-lg/7 overrides it.
leading-* is line height, tracking-* is letter spacing.
truncate and line-clamp-* limit long text; text-balance and text-pretty wrap it nicely.
Lists need list-disc or list-decimal back after Preflight.
A --font-* variable in @theme creates a font-* utility.
FlashcardsInteractive · disabled in the PDF
Leading
Line height: leading-6, leading-tight.
Tracking
Letter spacing: tracking-wide.
text-lg/7
Font size lg with a line height of 7 spacing steps.
truncate
One line, cut with an ellipsis.
line-clamp-2
At most two lines, then an ellipsis.
text-balance
Evens out line lengths, for headings.
Font fallback
The next font in the list, used if the first isn’t available.
Module 8: Colour, borders, corners and shadows
Tailwind CSS60 / 178
Module 8 · Colour, borders, corners and shadows
A palette of colours, each in eleven shades
Tailwind’s theme has a couple of dozen colour families (red, amber, green, sky, stone and more), each in eleven shades from 50 (nearly white) to 950 (nearly black). 500 is the middle. There are also black, white and transparent.
Shade
Typical use
50–100
Page and card backgrounds
200–300
Borders, dividers, subtle fills
400–600
Buttons, icons, accents
700–950
Text, dark backgrounds
The greys come in several warmths: slate and gray lean blue, zinc and neutral are flat, stone leans warm. A coffee shop might pair warm stone greys with amber browns.
Shade
One step of a colour family, from 50 (lightest) to 950 (darkest).
Palette
The full set of colours a design uses.
Tailwind CSS61 / 178
Module 8 · Colour, borders, corners and shadows
The same colour names work on every property
Prefix
Colours
bg-
Background
text-
Text
border-
Border
ring-, outline-
Rings and outlines
decoration-
Underlines
fill-, stroke-
Icons drawn in SVG
accent-
Checkboxes and sliders
shadow-
Shadow colour
HTML
1<divclass="bg-amber-100 text-amber-900 border border-amber-300">2 Free refills on filter coffee3</div>
Same family, three shades: a light fill, a mid border, dark text.
PlaygroundInteractive · disabled in the PDF
HTML
1<divclass="bg-amber-100 text-amber-900 border border-amber-300 p-4">2 Free refills on filter coffee3</div>
Swap amber for teal or rose in all three classes, then try bg-amber-400 text-amber-500 and see how hard it is to read.
QuizInteractive · disabled in the PDF
Which pairing is easiest to read?
Which pairing is easiest to read?
Tailwind CSS62 / 178
Module 8 · Colour, borders, corners and shadows
A slash sets a colour’s opacity
Add / and a percentage to any colour utility to make it see-through: bg-black/50 is black at 50%. Only that colour fades, unlike opacity-50, which fades the whole element, text and children included.
`bg-black/50`
Only the background is half-transparent
Text on it stays solid
`opacity-50`
The whole element is half-transparent
Text, images, children: everything fades
Fill the blanksInteractive · disabled in the PDF
Darken a photo with a black overlay at 40%, keeping the white headline on it solid.
HTML
1<divclass=" text-white">Fresh roast today</div>
Tailwind CSS63 / 178
Module 8 · Colour, borders, corners and shadows
Borders: width, side, style and colour
Class
Does
border
A 1px solid border on all sides
border-2, border-4
Thicker, in pixels
border-t, border-b, border-x
One side or one axis
border-dashed, border-dotted
A different line style
border-stone-200
The colour
divide-y
A line between each child, like rows in a menu
QuizInteractive · disabled in the PDF
In Tailwind 4, what colour is a plain border with no colour class?
In Tailwind 4, what colour is a plain border with no colour class?
Tailwind CSS64 / 178
Module 8 · Colour, borders, corners and shadows
Rounded corners
Class
Radius
rounded-xs
0.125rem
rounded-sm
0.25rem
rounded-md
0.375rem
rounded-lg
0.5rem
rounded-xl … rounded-4xl
0.75rem up to 2rem
rounded-full
A pill, or a circle on a square
Round one corner or one side with a position: rounded-t-lg (top two corners), rounded-bl-xl (bottom left). A photo inside a rounded card pokes out of the corners unless the card also has overflow-hidden.
HTML
1<articleclass="overflow-hidden rounded-2xl border border-stone-200">2<imgsrc="cortado.jpg"alt="A cortado in a small glass">3<h3class="p-4 font-semibold">Cortado</h3>4</article>5<imgsrc="barista.jpg"alt="Sam, our head barista"6class="size-16 rounded-full">
Tailwind CSS65 / 178
Module 8 · Colour, borders, corners and shadows
Shadows lift things off the page
shadow-2xs to shadow-2xl add a drop shadow, from barely there to deep. inset-shadow-* draws one inside the box instead, for pressed-in looks. Give a shadow a colour to make it warmer: shadow-lg shadow-amber-900/20.
What does shadow-amber-900/30 do on its own, with no shadow-md or similar?
What does shadow-amber-900/30 do on its own, with no shadow-md or similar?
Tailwind CSS66 / 178
Module 8 · Colour, borders, corners and shadows
Rings and outlines draw around without taking space
A border takes up room in the box model, so adding one can nudge the layout. A ring (ring, ring-2) is drawn with a shadow, and an outline (outline-2) is drawn outside the box: neither moves anything. That makes them ideal for showing keyboard focus. ring-offset-2 leaves a gap between the element and its ring.
The same card, live. Change shadow-sm to shadow-xl, rounded-2xl to rounded-none, or border-stone-200 to border-amber-500.
Tailwind CSS68 / 178
Module 8 · Colour, borders, corners and shadows
Try it: a card from a spec
Designers often hand over a list of styles. Turning that list into classes is most of the daily work with Tailwind. Here the component is written in React, where the attribute is className instead of class; the classes are the same.
Code challengeInteractive · disabled in the PDF
Style the card. The <article> needs rounded-xl border border-stone-200 bg-white p-6 shadow-md. The heading needs text-xl font-bold text-stone-900. The price needs text-amber-700.
TSX
1exportdefaultfunctionOfferCard(){2return(3<articleclassName="">4<h3className="">Seasonal blend</h3>5<pclassName="">12.00 a bag</p>6</article>7)8}
Tailwind CSS69 / 178
Module 8 · Colour, borders, corners and shadows
Recap
Colour families come in shades 50 to 950; pick text and background far apart for contrast.
The same colour names work with bg-, text-, border-, ring-, fill- and more.
bg-black/50 fades one colour; opacity-50 fades the whole element.
border is 1px and currentColor in Tailwind 4: add a colour.
rounded-* rounds corners; add overflow-hidden to clip images inside.
Shadows and rings need a size class; a colour class alone draws nothing.
Rings and outlines don’t move the layout, which makes them good for focus.
FlashcardsInteractive · disabled in the PDF
Contrast
The difference in lightness between text and its background.
/50 on a colour
That colour at 50% opacity.
opacity-50
The whole element, children included, at 50% opacity.
divide-y
A border between each child, stacked vertically.
Ring
A line around an element drawn as a shadow: takes no space.
overflow-hidden
Clips anything that sticks out of the box, such as image corners.
Module 9: Layout with flexbox
Tailwind CSS70 / 178
Module 9 · Layout with flexbox
Flexbox arranges children along a line
Block boxes stack top to bottom. To put a logo and some links side by side you need a layout system. Flexbox is the one for a single line of things: give a parent display: flex (the flex class) and its direct children become flex items, laid out along one line.
The main axis runs the way items flow; the cross axis runs across it.
Flex container
The parent with display: flex.
Flex item
A direct child of a flex container. Grandchildren are not affected.
Main axis
The direction items are placed in: across by default.
Change justify-between to justify-center, justify-end, justify-evenly. Then add flex-col h-48 next to flex: with height to spare, justify-* works top to bottom instead.
Tailwind CSS73 / 178
Module 9 · Layout with flexbox
items-* lines items up across the axis
In a row, items-* controls vertical alignment. The default, items-stretch, makes every item as tall as the tallest. items-center centres them, which is what you want for an icon next to text. items-start, items-end and items-baseline (line up the baselines the letters sit on) cover the rest.
QuizInteractive · disabled in the PDF
An icon and the word “Cart” sit in a flex row, but the icon looks too high. Which class on the row fixes it?
An icon and the word “Cart” sit in a flex row, but the icon looks too high. Which class on the row fixes it?
Tailwind CSS74 / 178
Module 9 · Layout with flexbox
Let items wrap onto new lines
By default flex items stay on one line and squeeze to fit. flex-wrap lets them move to a new line when they run out of room, which is handy for a row of tags. gap-* spaces them in both directions; gap-x-* and gap-y-* set each one.
Lines 1: A full-width header with a line under it.
Lines 2: The bar: a centred column, a flex row, items centred vertically, 1.5rem between them.
Lines 3: The shop’s name, first item.
Lines 4-7: The links, a flex row of their own.
Lines 8-9:ml-auto gives the button an automatic left margin, which takes all the leftover space and pushes it to the far right.
Tip.ml-auto on one item is the neatest way to split a flex row into “left group” and “right group”. justify-between works too, but spreads out every item.
Remove ml-auto and the button joins the links. Put it back, then try justify-between on the <nav> instead.
Tailwind CSS77 / 178
Module 9 · Layout with flexbox
Centring anything
Centring something both ways used to be a famous CSS puzzle. With flexbox it is two classes on the parent: one per axis.
Fill the blanksInteractive · disabled in the PDF
Centre the “Closed today” message horizontally and vertically in a full-height section.
HTML
1<sectionclass="flex min-h-dvh">2<p>Closed today. See you tomorrow.</p>3</section>
Tailwind CSS78 / 178
Module 9 · Layout with flexbox
Recap
flex makes a container; its direct children become items in a row.
flex-col turns the main axis vertical.
justify-* places items along the main axis; items-* across it.
flex-wrap and gap-* handle rows that run out of room.
flex-1 shares space, shrink-0 protects a size, min-w-0 lets text truncate.
ml-auto pushes an item, and everything after it, to the far end.
FlashcardsInteractive · disabled in the PDF
Flex item
A direct child of a flex container.
justify-between
First item at the start, last at the end, space shared between.
items-center
Centres items on the cross axis (vertically, in a row).
flex-1
Grow and shrink from zero: items share space equally.
Why min-w-0
Flex items won’t shrink below their content; this allows it.
ml-auto in a flex row
Takes all the free space on the left, pushing the item right.
Module 10: Grid and positioning
Tailwind CSS79 / 178
Module 10 · Grid and positioning
Grid lays things out in rows and columns at once
Flexbox handles one line. CSS grid handles two directions: you describe the columns (and optionally the rows), and children drop into the cells in order. It is the natural fit for a product grid, a photo gallery or a page with a sidebar.
Three columns: items fill the first row, then the next.
Grid container
The parent with display: grid (the grid class).
Track
One column or one row of the grid.
Cell
Where a row and a column cross.
`fr`
A fraction of the free space: 1fr 2fr gives the second column twice as much.
minmax(0, 1fr) means each column gets an equal share and may shrink to nothing, so long words don’t push one column wider than the others. For unequal columns, write them yourself: grid-cols-[200px_1fr] is a 200px sidebar and a column that takes the rest.
Change grid-cols-3 to grid-cols-2 or grid-cols-4, or to grid-cols-[120px_1fr_1fr]. Add col-span-2 to Chai.
QuizInteractive · disabled in the PDF
In the grid above, where does “Chai” go?
In the grid above, where does “Chai” go?
Tailwind CSS81 / 178
Module 10 · Grid and positioning
Items can span several cells
col-span-2 makes an item two columns wide; row-span-2 two rows tall. col-span-full stretches across every column. Use it for a featured product, or a heading inside the grid.
Fill the blanksInteractive · disabled in the PDF
Make the seasonal special take two columns and two rows of the product grid.
Instead of fixing three columns, you can say “as many columns as fit, each at least 14rem wide”. CSS does this with repeat(auto-fill, minmax(14rem, 1fr)). Tailwind has no named class for it, so write it in brackets, leaving out the spaces (or writing underscores where a space is needed).
One column on a phone, and more as the screen widens: no breakpoints needed.
QuizInteractive · disabled in the PDF
The grid above is 50rem wide with a 1.5rem gap. Roughly how many columns does it get?
The grid above is 50rem wide with a 1.5rem gap. Roughly how many columns does it get?
Tailwind CSS83 / 178
Module 10 · Grid and positioning
Flex or grid?
Flexbox
One direction at a time
Content decides the sizes
Navigation bars, button groups, a row of tags
An icon next to text
Grid
Rows and columns together
The layout decides the sizes
Product grids, galleries, page layouts
Things that must line up in both directions
Tip. They combine well: a grid of cards, where each card uses flex inside. When in doubt, ask whether the items need to line up with things in other rows. If yes, use grid.
QuizInteractive · disabled in the PDF
Which suits a photo gallery where every photo must line up in neat rows and columns?
Which suits a photo gallery where every photo must line up in neat rows and columns?
Tailwind CSS84 / 178
Module 10 · Grid and positioning
absolute places an element over another
Normally elements flow one after another. absolute takes an element out of that flow and places it with top-*, right-*, bottom-*, left-* (or inset-0 for all four), measured from the nearest ancestor that is relative. That pair is how you put a “New” badge on a product photo.
The amber box stands in for a photo. Change right-3 to left-3, or top-3 to bottom-3. Then delete relative and see where the badge goes.
QuizInteractive · disabled in the PDF
Remove relative from the outer div. Where does the badge go?
Remove relative from the outer div. Where does the badge go?
Tailwind CSS85 / 178
Module 10 · Grid and positioning
Sticky, fixed, and what sits on top
Class
Behaves
static
The default: in the normal flow
relative
In the flow; becomes the reference for absolute children
absolute
Out of the flow, placed from the nearest positioned ancestor
fixed
Placed from the window; stays put when you scroll
sticky
Scrolls normally until it hits its top-*, then sticks
When positioned elements overlap, z-* decides which is in front: higher numbers win (z-10, z-50). isolate keeps a component’s z-* values to itself, so they can’t fight the rest of the page.
Find the bugInteractive · disabled in the PDF
The header should stay at the top while the menu scrolls, but it scrolls away. Which line is wrong?
HTML
Tailwind CSS86 / 178
Module 10 · Grid and positioning
Recap
grid grid-cols-3 makes three equal columns; rows are added as needed.
col-span-* and row-span-* make an item cover several cells.
grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] fits as many columns as there is room for.
Flex for one direction and content-sized items; grid for things that line up both ways.
absolute places from the nearest relative ancestor; inset-0 covers it.
sticky top-0 keeps a header in view; z-* decides what is in front.
FlashcardsInteractive · disabled in the PDF
fr
A fraction of the free space in a grid.
grid-cols-3
repeat(3, minmax(0, 1fr)): three equal columns.
col-span-full
An item across every column.
auto-fill with minmax()
As many columns as fit, each at least the minimum.
relative + absolute
The child is placed relative to the parent.
sticky
Scrolls with the page until it reaches its top-*, then stays.
z-*
Stacking order: higher numbers are drawn in front.
Module 11: Responsive design and container queries
Tailwind CSS87 / 178
Module 11 · Responsive design and container queries
The same page, from a phone to a wide monitor
The coffee shop’s menu is read on 360px-wide phones and 1920px-wide monitors. Responsive design means one page whose layout adapts: one column on a phone, a grid on a desktop. It starts with one line in the HTML <head>, without which phones pretend to be a desktop and shrink everything.
Vite’s starter has it. Check any page you make by hand.
Viewport
The visible area of the page: the browser window, or a phone’s screen.
Breakpoint
A width at which the layout changes.
Responsive design
Layouts that adapt to the viewport instead of having one fixed size.
Tailwind CSS88 / 178
Module 11 · Responsive design and container queries
Mobile first: plain classes for every size, prefixes from a width up
A class with no prefix applies at every width. A breakpoint prefix like md: applies from that width and up. So you write the phone layout first, then add what changes on bigger screens. Tailwind writes each md: class inside @media (width >= 48rem).
One column on phones, two from 40rem, four from 64rem.
PlaygroundInteractive · disabled in the PDF
HTML
1<divclass="rounded-xl bg-rose-200 p-6 sm:bg-amber-2002 md:bg-lime-200 lg:bg-sky-200 xl:bg-violet-200">3<pclass="font-bold">Which breakpoint am I at?</p>4<p>Rose: none. Amber: sm. Lime: md. Sky: lg. Violet: xl.</p>5</div>
Breakpoints follow the viewport, which here is the preview’s own frame, only as wide as the slide, so you will probably see amber or lime. Delete md:bg-lime-200 and the colour falls back to the next smaller rule.
QuizInteractive · disabled in the PDF
What does sm:text-center do on a 360px phone?
What does sm:text-center do on a 360px phone?
Tailwind CSS89 / 178
Module 11 · Responsive design and container queries
Five breakpoints out of the box
Prefix
From
About
Think of
(none)
0
0px
Every screen
sm:
40rem
640px
Large phones
md:
48rem
768px
Tablets
lg:
64rem
1024px
Laptops
xl:
80rem
1280px
Desktops
2xl:
96rem
1536px
Wide monitors
Breakpoints are in rem, so they move if the reader enlarges their text: someone using 32px text on a laptop gets a layout meant for a smaller screen, which suits their huge letters better. Every utility and every variant can take a breakpoint prefix.
Tip. Pick breakpoints where your content breaks, not where particular devices are. Drag the window narrower in devtools (or use its device toolbar) and add a breakpoint where the layout starts to look wrong.
Tailwind CSS90 / 178
Module 11 · Responsive design and container queries
Showing and hiding by size
A common pattern: on phones the links collapse behind a menu button; on bigger screens they show in the bar and the button goes. hidden is display: none; md:flex brings the list back as a flex row from 48rem.
Fill the blanksInteractive · disabled in the PDF
Hide the link list on phones and show it as a flex row from md; do the opposite for the menu button.
Keep them in one unit. Tailwind sorts breakpoints by their values to put the media queries in the right order. Mixing rem and px breakpoints can make that order wrong, and then a larger breakpoint’s styles may lose to a smaller one’s.
Tailwind CSS93 / 178
Module 11 · Responsive design and container queries
Container queries respond to the space a component gets
A menu card might sit in a narrow sidebar on one page and a wide column on another, at the same screen width. Breakpoints only know the screen, so they can’t help. A container query asks how wide the parent is. Mark the parent with @container, then use @sm:, @md: and so on in its children. They are built into Tailwind 4.
The dashed box is the container, 20rem wide, so the card is stacked. Drag its bottom-right corner wider (or change w-xs to w-lg): past 28rem the card goes side by side.
QuizInteractive · disabled in the PDF
The same card appears in a 20rem sidebar and a 40rem main column, on a 1280px screen. With @md:flex-row, how does it look?
The same card appears in a 20rem sidebar and a 40rem main column, on a 1280px screen. With @md:flex-row, how does it look?
Tailwind CSS94 / 178
Module 11 · Responsive design and container queries
Container sizes, ranges and names
Variant
Container width
@3xs: … @xs:
16rem to 20rem
@sm:, @md:, @lg:
24rem, 28rem, 32rem
@xl: … @7xl:
36rem to 80rem
@max-md:
Narrower than 28rem
@min-[400px]:
A one-off size
Containers nest, and a child answers to its nearest one. To target a specific ancestor, name it with a slash: @container/menu on the parent, @lg/menu: on the child.
Expert note.@container sets container-type: inline-size, which stops the element from sizing itself to its children’s width. A container that should shrink-wrap its content (in a flex row with no width, say) may collapse to zero. Give containers a width from their own parent, as block elements or flex-1 items have.
Tailwind CSS95 / 178
Module 11 · Responsive design and container queries
Recap
Add the viewport <meta> tag, or phones won’t behave responsively.
sm 40rem, md 48rem, lg 64rem, xl 80rem, 2xl 96rem.
max-md: is “below md”; md:max-lg: is a range; min-[600px]: is a one-off.
New breakpoints are --breakpoint-* variables in @theme.
@container on a parent and @md: on children respond to the parent’s width.
FlashcardsInteractive · disabled in the PDF
Mobile-first
Unprefixed classes for phones, prefixes to override on wider screens.
md:
From 48rem (768px) and wider.
max-md:
Narrower than 48rem.
Container query
A style that depends on the parent’s width, not the screen’s.
@container/name
A named container, targeted with @md/name:.
Why breakpoints use rem
They move with the reader’s text size, giving big text roomier layouts.
Module 12: States: hover, focus and friends
Tailwind CSS96 / 178
Module 12 · States: hover, focus and friends
A variant is a condition in front of a utility
You met hover: and md: already. Every variant works the same way: variant:utility means “apply this utility only when the condition holds”. Tailwind turns the variant into a pseudo-class, a media query or a selector around the utility’s rule.
Two details. The colon in the class name is escaped with a backslash in CSS, because a bare colon means a pseudo-class. And in Tailwind 4, hover: only applies on devices that can really hover, so a tap on a phone doesn’t leave a button stuck in its hover colour.
Tailwind CSS97 / 178
Module 12 · States: hover, focus and friends
Focus shows where the keyboard is
Many people move through pages with the Tab key instead of a mouse: people with motor impairments, screen reader users, fast typists. The element that would receive a key press has focus. focus: applies whenever an element is focused; focus-visible: applies only when the browser thinks a focus indicator is helpful, which mostly means keyboard use.
Note. Tailwind 4’s Preflight gives buttons the browser’s default arrow cursor, not a pointing hand. Add cursor-pointer if your design wants the hand.
QuizInteractive · disabled in the PDF
What does md:hover:bg-amber-700 mean?
What does md:hover:bg-amber-700 mean?
Tailwind CSS99 / 178
Module 12 · States: hover, focus and friends
group: style a child when its parent is hovered
Hovering anywhere on a product card should underline its title. The title isn’t hovered, the card is. Mark the parent with the class group, and use group-hover: on the child. Any state works: group-focus-within:, group-active: and so on.
Hover anywhere on the card: the title underlines. Remove group from the link and the children stop reacting. Try group-hover:text-amber-700 on the title.
When groups nest, name them with a slash: group/card on the parent and group-hover/card: on the child, so a child answers to the right ancestor.
Fill the blanksInteractive · disabled in the PDF
Show the arrow only while the card is hovered, using a group named card.
HTML
1<aclass=" flex justify-between">2 Order ahead3<spanclass="opacity-0 :opacity-100">→</span>4</a>
Tailwind CSS100 / 178
Module 12 · States: hover, focus and friends
peer: style an element by a sibling’s state
Sometimes the trigger is a sibling, not a parent: a message that appears when the input before it is invalid, or a label that changes when a checkbox is ticked. Mark the sibling with peer and use peer-*: variants. CSS can only look forward through siblings, so the peer must come before the element that reacts.
HTML
1<label>2<inputtype="checkbox"class="peer sr-only">3<spanclass="rounded-full border px-3 py-14 peer-checked:bg-amber-600 peer-checked:text-white">5 Extra shot6</span>7</label>
sr-only hides the real checkbox visually but keeps it for screen readers and keyboards.
Click the pill to tick the hidden checkbox. Change peer-checked:bg-amber-600 to peer-checked:bg-teal-600, or delete sr-only to see the real checkbox.
Find the bugInteractive · disabled in the PDF
The warning should appear when the email is invalid, but it never does. Which line is the problem?
HTML
Tailwind CSS101 / 178
Module 12 · States: hover, focus and friends
has-*, not-* and in-*
Variant
Matches when
Example
has-checked:
The element contains a checked input
A whole option card highlights
has-[img]:
The element contains an <img>
Different padding for cards with photos
group-has-[…]:
The group contains something
A badge reacts to the card’s content
not-last:
The opposite of a variant: not the last child
A line under every row but the last
in-focus:
Any ancestor is focused, without needing group
Text inside a focused card
HTML
1<labelclass="flex gap-2 rounded-lg border p-32 has-checked:border-amber-600 has-checked:bg-amber-50">3<inputtype="radio"name="size"value="large">4 Large (16oz)5</label>
The parent reacts to its own child: no peer, no JavaScript.
QuizInteractive · disabled in the PDF
Menu rows need a divider under each, except the last. Which class goes on each row?
Menu rows need a divider under each, except the last. Which class goes on each row?
Tailwind CSS102 / 178
Module 12 · States: hover, focus and friends
Styling from data-* and aria-* attributes
Interactive parts often record their state in attributes: aria-expanded="true" on a menu button, data-state="open" set by a script. ARIA attributes describe state to assistive technology, and data attributes are your own. Variants read both, so the style follows the real state.
Tip. Prefer aria-* variants when the attribute already exists for accessibility: one attribute then drives both what screen readers announce and what the eye sees, and they can’t drift apart.
QuizInteractive · disabled in the PDF
A script sets aria-expanded="true" on the button when the menu opens. Which of the code’s classes take effect?
A script sets aria-expanded="true" on the button when the menu opens. Which of the code’s classes take effect?
Tailwind CSS103 / 178
Module 12 · States: hover, focus and friends
Try it: a toggle button styled by its state
A toggle button tells assistive technology whether it is on with aria-pressed. With aria-pressed: variants, the same attribute sets its look, so what people see and what screen readers announce always agree.
Code challengeInteractive · disabled in the PDF
Make the Favourite button a toggle. Keep a pressed state (starting false), set aria-pressed={pressed} on the button, and flip it on each click. Add aria-pressed:bg-amber-600 and aria-pressed:text-white to its classes.
TSX
1import{ useState }from'react'23exportdefaultfunctionFavouriteButton(){4// TODO: a pressed state, aria-pressed, and a click handler5return(6<button7type="button"8className="rounded-full border border-amber-600 px-4 py-29 text-amber-700"10>11 Favourite12</button>13)14}
Tailwind CSS104 / 178
Module 12 · States: hover, focus and friends
Parts and positions: placeholder, first, odd, before
*: styles children from the parent. *:odd: reads left to right: each child, when it is odd.
Expert note. Variants apply from left to right in Tailwind 4. odd:*: would mean “the children of this list, when the list itself is odd”, a different thing. Tailwind 3 read stacked variants the other way round, so this matters when upgrading old code.
Tailwind CSS105 / 178
Module 12 · States: hover, focus and friends
Recap
variant:utility applies the utility only when the variant’s condition holds; variants stack.
hover: only fires on devices that can hover; focus-visible: is for keyboard focus.
Never remove focus outlines without a visible replacement.
group + group-hover: reacts to a parent; peer + peer-*: to an earlier sibling.
has-*: reacts to what an element contains; not-*: negates any variant.
aria-*: and data-*: follow attributes, so styles match the real state.
FlashcardsInteractive · disabled in the PDF
Focus
The element that will receive keyboard input.
focus-visible:
Focused, and the browser judges an indicator useful (mostly keyboard).
group-hover:
Applies when an ancestor with the group class is hovered.
peer-checked:
Applies when an earlier sibling with peer is checked.
has-checked:
Applies when the element contains a checked input.
ARIA attribute
An aria-* attribute that describes state or meaning to assistive technology.
sr-only
Visually hidden, but still read by screen readers.
Module 13: Dark mode
Tailwind CSS106 / 178
Module 13 · Dark mode
Dark mode follows a setting people choose
Phones and computers let people pick a light or dark appearance. Browsers pass that choice to CSS through a media query, prefers-color-scheme. A site that respects it is easier on the eyes at night and feels at home on the device.
Each dark: class overrides its light partner when the system is dark. By default Tailwind 4 writes dark: as @media (prefers-color-scheme: dark), so it works with no setup.
PlaygroundInteractive · disabled in the PDF
HTML
1<divclass="bg-stone-50 p-6 text-stone-9002 dark:bg-stone-900 dark:text-stone-100">3<articleclass="rounded-xl border border-stone-200 bg-white p-64 dark:border-stone-700 dark:bg-stone-800">5<h2class="text-amber-800 dark:text-amber-300">Today’s roast</h2>6<pclass="dark:hidden">Your system is in light mode.</p>7<pclass="hidden dark:block">Your system is in dark mode.</p>8</article>9</div>
The preview follows your system setting, and says which one it sees. Change the classes for the mode you are in (bg-white, or dark:bg-stone-800), or switch your system’s appearance and watch the other set take over.
QuizInteractive · disabled in the PDF
In dark mode, what background does the <article> have?
In dark mode, what background does the <article> have?
Tailwind CSS108 / 178
Module 13 · Dark mode
Dark mode is not just inverted colours
Avoid pure black backgrounds and pure white text: very dark grey and off-white are gentler.
Accent colours usually need a lighter shade in the dark: amber-800 text becomes amber-300.
Shadows barely show on dark backgrounds; use lighter surfaces or borders for depth.
Check contrast again: a pair that passes in light mode can fail in dark.
Also tell the browser which scheme the page is in, so its own parts (scrollbars, form controls, the date picker) match. scheme-light and scheme-dark set the CSS color-scheme property.
HTML
1<htmlclass="scheme-light dark:scheme-dark">
Tailwind CSS109 / 178
Module 13 · Dark mode
A toggle needs dark: to follow a class
Many sites offer a button to override the system setting. Then dark: must follow something you control, such as a dark class on <html>, instead of the media query. @custom-variant redefines a variant in your CSS.
WalkthroughInteractive · disabled in the PDF
src/style.css
1@import"tailwindcss";23@custom-variant dark (&:where(.dark, .dark *));
Lines 1: Load Tailwind as usual.
Lines 3:@custom-variant dark replaces the built-in dark variant.
Lines 3:& stands for the element with the class. &:where(.dark, .dark *) matches it when it is.dark or is inside.dark. :where() adds no specificity, so dark: classes stay as easy to override as any other.
Fill the blanksInteractive · disabled in the PDF
Make dark: follow a data-theme="dark" attribute instead of a class.
1<head>2 <script>3const saved = localStorage.getItem('theme')4const system =matchMedia('(prefers-color-scheme: dark)')5const dark = saved ? saved ==='dark': system.matches6 document.documentElement.classList.toggle('dark', dark)7</script>8</head>
Lines 2,7: A <script> in the <head> runs before the page is drawn, so there is no flash of the wrong theme.
Lines 3:localStorage is a small store in the browser that survives reloads. Read any saved choice.
Lines 4: Ask the browser whether the system prefers dark.
Lines 5: Use the saved choice if there is one, otherwise the system’s.
Lines 6: Add the dark class to <html> if dark, remove it if not.
src/main.js
1const button = document.querySelector('#theme-toggle')2button.addEventListener('click',()=>{3const root = document.documentElement4const dark = root.classList.toggle('dark')5 localStorage.setItem('theme', dark ?'dark':'light')6})
Clicking the button flips the class and saves the new choice.
Tailwind CSS111 / 178
Module 13 · Dark mode
Practice: what happens on a visit
Put in orderInteractive · disabled in the PDF
A reader who chose dark last time comes back. Put what happens in order.
1.It adds the dark class to <html>
2.The browser starts reading the <head>
3.The stylesheet’s dark: rules now match
4.The inline script reads theme from localStorage
5.The page is painted, already dark
Tip. Offer three choices, not two: light, dark and “system”. Choosing “system” removes the saved value, so the site follows the device again.
Tailwind CSS112 / 178
Module 13 · Dark mode
Variables switch every colour at once
Writing dark: next to every colour doubles the classes and makes it easy to miss one. The alternative is to name colours by their role (page, ink, surface) as theme variables, then give those variables different values in dark mode. One class, bg-page, is then right in both modes.
src/style.css
1@import"tailwindcss";2@custom-variant dark (&:where(.dark, .dark *));34@theme{5 --color-page:oklch(0.980.0180);6 --color-ink:oklch(0.250.0250);7}89@layer base {10.dark{11 --color-page:oklch(0.20.0150);12 --color-ink:oklch(0.950.0180);13}14}
bg-page and text-ink read the variables, so they change when .dark changes them.
Both panels have the same utilities; only the variables differ. Change --page in .night to oklch(0.3 0.08 260). The same idea, with @theme and .dark, gives you bg-page.
Find the bugInteractive · disabled in the PDF
With the CSS above, the page switches to dark, but one card stays white. Which line?
HTML
Tailwind CSS113 / 178
Module 13 · Dark mode
Recap
Dark mode follows prefers-color-scheme; dark: uses it by default.
Pair every light colour with a dark one, and re-check contrast.
scheme-dark makes the browser’s own controls match.
@custom-variant dark (&:where(.dark, .dark *)); makes dark: follow a class.
A script in the <head> applies the saved choice before the first paint.
Role-named colour variables, redefined under .dark, switch everything at once.
FlashcardsInteractive · disabled in the PDF
prefers-color-scheme
The media feature that reports the device’s light or dark setting.
@custom-variant
Defines or redefines a variant in CSS.
:where()
A selector wrapper that adds no specificity.
localStorage
A small key-value store in the browser that survives reloads.
Flash of the wrong theme
Avoided by setting the class in a <head> script, before paint.
Role-based colour
A colour named by its job (page, ink), not its hue.
Module 14: Forms and accessibility
Tailwind CSS114 / 178
Module 14 · Forms and accessibility
The parts of a form
The coffee shop wants a newsletter sign-up. A form groups fields that are sent together. Each field is a form control: <input> for text, email and checkboxes, <select> for a list of choices, <textarea> for longer text, and <button> to send.
Any element a person fills in or operates: inputs, selects, buttons.
`type`
What kind of input: email brings an email keyboard on phones and checks the format.
`required`
The form won’t send while this field is empty.
Accessibility
Making something usable by people with disabilities: often shortened to a11y.
Tailwind CSS115 / 178
Module 14 · Forms and accessibility
Every field needs a real label
A <label> with a for that matches the input’s id does three things: screen readers announce it when the field is focused, clicking it focuses the field (a bigger target), and it stays visible while typing. A placeholder does none of these reliably, and it disappears as soon as someone types.
Preflight strips most of the browser’s styling from form controls: they inherit the page font and lose their borders and backgrounds. You decide the look.
Lines 1-3: A small, medium-weight label on its own line (block).
Lines 4-5: Full width, rounded, with a visible border: people need to see where to type.
Lines 6: Comfortable padding, and a placeholder that is clearly lighter than real text.
Lines 7-8: A clear focus state: darker border plus a soft outline around it.
Tip. For text fields, focus: is fine instead of focus-visible:: browsers show the focus state on text inputs for mouse users too, because people need to know where their typing will go.
Click the label: the field gets focus. Change the focus colours to sky, try rounded-full, or remove border and see how hard the field is to find.
Tailwind CSS117 / 178
Module 14 · Forms and accessibility
Validation states without JavaScript
The browser already knows whether a field is valid, from attributes like required, type="email" and minlength. invalid: styles a field the moment it is invalid, which means an empty required field looks like an error before anyone has typed. user-invalid: waits until the person has interacted with the field, which is kinder.
HTML
1<inputid="email"type="email"required2class="peer rounded-lg border border-stone-300 px-3 py-23 user-invalid:border-red-600">4<pclass="mt-1 hidden text-sm text-red-700 peer-user-invalid:block">5 Please enter an email address like name@example.com.6</p>
PlaygroundInteractive · disabled in the PDF
HTML
1<formclass="max-w-sm">2<labelfor="email"class="block text-sm font-medium">Email</label>3<inputid="email"type="email"required4class="peer mt-1 w-full rounded-lg border border-stone-300 px-35 py-2 user-invalid:border-red-600">6<pclass="mt-1 hidden text-sm text-red-700 peer-user-invalid:block">7 Please enter an email address like name@example.com.8</p>9</form>
Type coffee, then click outside the field: the border turns red and the message appears. Now change user-invalid: to invalid: in both places and see the error before you type.
QuizInteractive · disabled in the PDF
A form loads with an empty required email field styled invalid:border-red-600. What does a new visitor see?
A form loads with an empty required email field styled invalid:border-red-600. What does a new visitor see?
Tailwind CSS118 / 178
Module 14 · Forms and accessibility
Try it: a field that shows its error
When the error comes from your own code (the server said the email is taken, say), a prop switches the field’s classes and attributes. The red border is for the eye; aria-invalid and aria-describedby tell a screen reader the same thing and read the message out.
Code challengeInteractive · disabled in the PDF
When error is given, give the input border-red-600 instead of border-stone-300, set aria-invalid to true and aria-describedby="email-error", and show the error in <p id="email-error" className="mt-1 text-sm text-red-700">. Without an error, show no message.
The quickest way to brand a checkbox or radio is accent-*, which recolours the browser’s own control and keeps all its accessibility. For a fully custom look, appearance-none removes the native drawing so you can style the box yourself with checked: variants.
Fill the blanksInteractive · disabled in the PDF
Make the checkbox amber, with the label clickable next to it.
HTML
1<labelclass="flex items-center gap-2">2<inputtype="checkbox"class="size-4 ">3 Send me the monthly roast notes4</label>
The official forms plugin. Tailwind’s @tailwindcss/forms plugin gives every control a consistent, easy-to-override starting style. In Tailwind 4 you load a plugin from CSS: @plugin "@tailwindcss/forms"; after the import.
Tailwind CSS120 / 178
Module 14 · Forms and accessibility
Accessibility habits for every component
Use the right element. A <button> is focusable and works with Enter and Space; a <div> with a click handler is neither.
Keep focus visible. Style focus-visible:, never just remove outlines.
Check contrast. At least 4.5:1 for body text, 3:1 for large text and for the edges of controls.
Don’t rely on colour alone. Pair a red border with an error message or an icon.
Make targets big enough. Around 24px or more for anything tappable; p-2 on an icon button helps.
Hide visually, not from everyone.sr-only keeps text for screen readers; hidden removes it for all.
Predict the outputInteractive · disabled in the PDF
A cart button shows only an icon. How do you give it a name a screen reader can say?
A cart button shows only an icon. How do you give it a name a screen reader can say?
Tailwind CSS121 / 178
Module 14 · Forms and accessibility
Respecting contrast and colour settings
Variant
Matches when
contrast-more:
The person asked their system for more contrast
forced-colors:
A high-contrast mode replaces the page’s colours (as on Windows)
motion-reduce:
They asked for less motion (next module)
print:
The page is being printed
HTML
1<pclass="text-stone-500 contrast-more:text-stone-800">2 Prices include tax.3</p>4<buttonclass="bg-amber-600 forced-colors:border">Order</button>
In forced colours mode backgrounds may vanish; a border keeps the button’s shape.
Tailwind CSS122 / 178
Module 14 · Forms and accessibility
Practice: review a sign-up form
Find the bugInteractive · disabled in the PDF
This form looks fine with a mouse. One line makes it unusable for keyboard users. Which?
HTML
Tailwind CSS123 / 178
Module 14 · Forms and accessibility
Recap
Every control needs a <label>, linked by for/id or by wrapping. Placeholders are not labels.
Preflight strips form styling: give fields a visible border, padding and a clear focus state.
user-invalid: shows errors after interaction; invalid: shows them immediately.
accent-* recolours native checkboxes and radios.
Use real elements, visible focus, enough contrast, and more than colour to signal errors.
sr-only hides from the eye only; hidden hides from everyone.
contrast-more:, forced-colors: and print: adapt to people’s settings.
FlashcardsInteractive · disabled in the PDF
a11y
Accessibility: “a”, 11 letters, “y”.
for on a label
The id of the input it names.
user-invalid:
Invalid, after the person has interacted with the field.
accent-*
Sets accent-color to recolour native checkboxes, radios and sliders.
appearance-none
Removes a control’s native look so you can draw your own.
forced-colors:
Matches when a high-contrast mode overrides the page’s colours.
Contrast ratio for body text
At least 4.5:1.
Module 15: Transitions, transforms and animation
Tailwind CSS124 / 178
Module 15 · Transitions, transforms and animation
A transition animates the change between two states
Without help, a button’s colour jumps the instant it is hovered. A transition tells the browser to animate the change over a short time. Put the transition on the element itself, not on the hover: state, so it animates both in and out.
Class
Does
transition
Animates the common properties: colours, opacity, shadows, transforms
How speed changes over that time: ease-out starts fast and slows down.
Tailwind CSS125 / 178
Module 15 · Transitions, transforms and animation
Transforms move, scale and rotate without disturbing the layout
Class
Does
scale-105
105% of its size
-translate-y-1
Moves up by one spacing step
translate-x-full
Moves right by its own width
rotate-3, -rotate-6
Turns by degrees
origin-top-left
The point it scales and turns around
A transformed element is drawn moved or bigger, but the space it holds in the layout doesn’t change, so neighbours don’t shift. Tailwind 4 writes them with the CSS translate, scale and rotate properties, which combine on their own: no separate transform class is needed.
QuizInteractive · disabled in the PDF
A product card gets hover:scale-105. What happens to the cards next to it?
A product card gets hover:scale-105. What happens to the cards next to it?
Hover the card. Change duration-200 to duration-1000 to watch it slowly, try hover:-translate-y-3, or delete transition and see it jump.
Tip. Keep interface transitions short: 150 to 300ms. Longer feels sluggish when someone moves the mouse across many items.
Tailwind CSS127 / 178
Module 15 · Transitions, transforms and animation
Animations run on their own
A transition needs a change to react to. An animation plays by itself, following keyframes: a list of what the element looks like at points in time. Tailwind ships four.
A spinner, a notification dot and a loading skeleton. Swap animate-spin for animate-bounce, or remove border-t-amber-600 and see why the spinner needs one different side.
QuizInteractive · disabled in the PDF
Which is the right tool for a button that darkens smoothly when hovered?
Which is the right tool for a button that darkens smoothly when hovered?
Tailwind CSS128 / 178
Module 15 · Transitions, transforms and animation
Your own animation lives in the theme
Add a --animate-* variable for the utility, and put the @keyframes inside @theme too. Tailwind only writes the keyframes into your CSS if something uses the animation.
Fill the blanksInteractive · disabled in the PDF
Create an animate-steam utility for the steam above a coffee cup.
A transition needs a “before” state, but an element that has just appeared has none. CSS solves this with @starting-style, and Tailwind exposes it as the starting: variant: the values to start from when the element first shows. To animate something that switches display (like a popover or a <dialog>), add transition-discrete so display changes at the right moment.
HTML
1<divpopoverid="offer"class="rounded-xl p-6 shadow-xl2 transition-discrete transition duration-3003 opacity-100 starting:open:opacity-0">4 Today only: a free pastry with any large drink.5</div>6<buttonpopovertarget="offer">See offer</button>
popover and popovertarget are HTML features: the button opens the box, no JavaScript.
Note.@starting-style is supported in current versions of all major browsers. Where it isn’t, the element simply appears without the fade, which is a safe fallback.
Tailwind CSS130 / 178
Module 15 · Transitions, transforms and animation
Less motion for people who ask for it
Movement on screen can cause dizziness or nausea for people with vestibular disorders, and some find it distracting. Systems have a “reduce motion” setting. motion-reduce: applies when it is on; motion-safe: applies only when it is off.
The hero’s steam animation should only play for people who haven’t asked for reduced motion. Which line is wrong?
HTML
Tailwind CSS131 / 178
Module 15 · Transitions, transforms and animation
Animate the cheap properties
To animate width, height, top or margin, the browser must redo the layout of the page every frame, which stutters on slower phones. opacity and the transform properties (translate, scale, rotate) can usually be handled by the graphics chip alone, so they stay smooth.
QuizInteractive · disabled in the PDF
A cart drawer should slide in from the right. Which is smoothest?
A cart drawer should slide in from the right. Which is smoothest?
Expert note.will-change-transform asks the browser to prepare an element for animation in advance. Use it only on something that is about to animate and showing jank: on many elements it uses up memory and can make things slower.
Tailwind CSS132 / 178
Module 15 · Transitions, transforms and animation
Recap
Transitions animate a change of state; put transition on the element, the new value on hover:.
duration-*, ease-* and delay-* shape the timing; keep it around 150 to 300ms.
Transforms (scale-*, translate-*, rotate-*) move things without shifting the layout.
Animations play on their own: animate-spin, animate-pulse, or your own --animate-*.
starting: gives an appearing element a state to transition from.
Wrap decorative motion in motion-safe:; provide motion-reduce: fallbacks.
Animate opacity and transforms, not width, top or margin.
FlashcardsInteractive · disabled in the PDF
Transition
An animated change from one value to another when a state changes.
Animation
Motion that plays on its own, following keyframes.
Keyframes
What an element looks like at points in an animation: 0%, 50%, 100%.
starting:
Values an element transitions from when it first appears (@starting-style).
motion-safe:
Applies only when the person has not asked for reduced motion.
Cheap properties to animate
opacity, translate, scale, rotate.
Module 16: Theming with @theme and design tokens
Tailwind CSS133 / 178
Module 16 · Theming with @theme and design tokens
A theme is a set of named design decisions
A design token is one decision with a name: “our brown is this exact colour”, “cards have this radius”. Designers and developers share the names, so the same decision means the same thing everywhere. In Tailwind 4 the theme is a list of tokens, written as CSS variables inside @theme.
Each variable does two jobs. It creates utilities (bg-espresso, text-crema, font-display, rounded-card), and once something uses it, it is written into your CSS as a normal variable on :root, so any CSS or JavaScript can read it.
Tailwind CSS134 / 178
Module 16 · Theming with @theme and design tokens
The name’s prefix decides which utilities appear
Namespace
Creates
Example
--color-*
Every colour utility
bg-, text-, border-, fill-…
--font-*
Font families
font-display
--text-*
Font sizes
text-hero
--font-weight-*
Weights
font-heavy
--spacing
The base step for every spacing and size utility
p-4, w-12, gap-2
--radius-*
Corner radius
rounded-card
--shadow-*
Shadows
shadow-soft
--breakpoint-*
Responsive variants
3xl:
--container-*
Container variants and max-w-*
@4xl:, max-w-4xl
--ease-*, --animate-*
Timing curves, animations
ease-snappy, animate-steam
QuizInteractive · disabled in the PDF
You add --colour-latte: oklch(0.8 0.05 70); to @theme. What does bg-latte do?
You add --colour-latte: oklch(0.8 0.05 70); to @theme. What does bg-latte do?
Tailwind CSS135 / 178
Module 16 · Theming with @theme and design tokens
Extending the theme adds to the defaults
Adding a variable keeps everything Tailwind already has: you get bg-espressoandbg-amber-600. To change a default, redefine it with the same name; your value wins because your @theme comes after Tailwind’s.
Fill the blanksInteractive · disabled in the PDF
Add a text-hero size of 4.5rem with a tight line height of 1, and make the default rounded-lg a little larger.
A design system often wants only its own colours, so nobody can reach for bg-pink-500. Set the namespace to initial first: that removes Tailwind’s values for it, and only yours remain. --*: initial clears the whole theme.
With the theme above, a teammate writes text-stone-600. What happens?
With the theme above, a teammate writes text-stone-600. What happens?
Tailwind CSS137 / 178
Module 16 · Theming with @theme and design tokens
@theme versus :root
`@theme`
Creates utilities and variants
Must be at the top level of the file, not nested
Only plain variables (and @keyframes) inside
For design tokens
`:root` (or any selector)
Just a CSS variable, no utilities
Can be anywhere, inside media queries or .dark
Can change at runtime
For values that aren’t tokens, or to override tokens
They work together. @theme declares a token, and a selector such as .dark or [data-brand=night] gives it another value in part of the page. The utility reads var(--color-espresso), so it follows whichever value applies.
Note. To save space, Tailwind only writes the theme variables your CSS actually uses into the output. @theme static { … } writes them all, which you want when JavaScript or another stylesheet reads variables that no utility uses.
Tailwind CSS138 / 178
Module 16 · Theming with @theme and design tokens
@theme inline for tokens that point to other variables
A semantic token names a job (--color-primary) and points to a primitive token that names a value (--color-espresso). If you write that with a plain @theme, the utility reads var(--color-primary), which is resolved where the variable was defined, on :root. Overriding --color-espresso lower down the page wouldn’t reach it. @theme inline writes the reference straight into the utility instead.
The reason is how CSS variables work. A variable whose value is var(--other) is computed on the element where it is declared, and the computed result is what children inherit. So --color-primary on :root is already “espresso as :root sees it”. A .night { --color-espresso: … } further down changes --color-espresso there, but children still inherit the computed --color-primary from :root. Inlining removes the middle step.
Tailwind CSS139 / 178
Module 16 · Theming with @theme and design tokens
Using tokens in your own CSS
Tokens are variables, so plain var() works anywhere. Tailwind also adds small functions to CSS you write in the same file: --spacing(n) for a spacing step, and --alpha(colour / amount) for a see-through colour.
Primitive tokens--color-espresso, --color-foam: raw values
Each layer only points at the one below it.
Components use semantic names (bg-primary, text-danger), never raw ones. A rebrand or a dark theme then changes only what the semantic tokens point at, and every component follows. Most sites need the first two layers; component tokens are for large libraries.
QuizInteractive · disabled in the PDF
Which class is the most future-proof for the error text under a form field?
Which class is the most future-proof for the error text under a form field?
Tailwind CSS141 / 178
Module 16 · Theming with @theme and design tokens
Recap
Design tokens are named decisions; in Tailwind 4 they are variables in @theme.
The namespace (--color-*, --font-*, --radius-*…) decides which utilities appear.
Adding extends the defaults; reusing a name overrides one; --color-*: initial clears a namespace.
@theme is top-level only; override values in normal selectors such as .dark.
@theme inline for tokens that point at other variables; @theme static to emit every variable.
var(), --spacing() and --alpha() use tokens in your own CSS.
Components use semantic tokens that point at primitives.
FlashcardsInteractive · disabled in the PDF
Design token
A named design decision, such as a colour or radius.
Namespace
The prefix of a theme variable (--color-) that decides its utilities.
--color-*: initial
Removes all default colours so only yours remain.
@theme inline
Utilities use the variable’s value directly instead of a var() to it.
@theme static
Writes every theme variable, used or not.
Primitive vs semantic token
A raw value (espresso) vs a role (primary) that points at one.
--alpha()
A Tailwind CSS function for a colour at some opacity.
Module 17: Components, reuse and @apply
Tailwind CSS142 / 178
Module 17 · Components, reuse and @apply
Repetition is fine until it has to change together
Ten menu items with the same classes are usually one line in a loop, so there is nothing to extract. Editors can also change several copies at once. Extract when the same group of classes appears in different files and must stay identical: every button on the site, every product card.
Generated by a loop? Leave it: the classes are written once.
Same element, several places, changes together? Make a component.
HTML you don’t write (from a CMS, Markdown, a third-party widget)? Style it with CSS: see @apply below.
Component
A reusable piece of interface, written once and used in many places: a template partial, or a component in a framework such as React, Vue, Svelte or Astro.
Extract
Move repeated code into one shared place.
Tailwind CSS143 / 178
Module 17 · Components, reuse and @apply
A component keeps the classes in one file
Every framework has components. This one is written in JSX, the HTML-like syntax React uses; className is how JSX spells class. The idea is the same in any framework: a function or template that takes some inputs and returns markup.
Used as <Button>Order</Button>. Change the look here and every button follows.
PlaygroundInteractive · disabled in the PDF
TSX
1importtype{ ReactNode }from'react'23functionButton({ children }:{ children:ReactNode}){4return(5<button6className="rounded-lg bg-amber-600 px-4 py-2 font-semibold7 text-white transition-colors hover:bg-amber-700"8>9{children}10</button>11)12}1314exportdefaultfunctionMenu(){15return(16<divclassName="flex gap-2">17<Button>Order</Button>18<Button>Book a table</Button>19<Button>Gift card</Button>20</div>21)22}
Change rounded-lg to rounded-full or bg-amber-600 to bg-teal-600 once, in Button: all three buttons follow.
Tailwind CSS144 / 178
Module 17 · Components, reuse and @apply
Variants of a component: map to complete class names
Buttons come in kinds: primary, secondary, danger. Keep a lookup of complete class strings for each kind and pick one. Every class is written out in full in the file, so Tailwind’s scanner can see it.
The badge colour comes from the data, but no badge ever has a background. Which line?
TSX
Tailwind CSS145 / 178
Module 17 · Components, reuse and @apply
Try it: a button with kinds
Finish the lookup of kinds and use it. The shared classes stay in the button; only the colours change with kind.
Code challengeInteractive · disabled in the PDF
Fill in kinds: secondary is bg-white text-stone-900 ring-1 ring-stone-300, danger is bg-red-600 text-white hover:bg-red-700. Then add kinds[kind] to the button’s classes, after rounded-lg px-4 py-2.
All utilities have the same specificity, one class. So when two set the same property, the one that comes later in the CSS file wins. The order in the class attribute means nothing: class="p-2 p-4" and class="p-4 p-2" look identical.
QuizInteractive · disabled in the PDF
A Button component has px-4, and someone uses it as <Button className="px-8">, which appends px-8. How much side padding does it get?
A Button component has px-4, and someone uses it as <Button className="px-8">, which appends px-8. How much side padding does it get?
Tip. The editor extension flags conflicting classes on the same element (“p-2 and p-4 apply the same CSS properties”). Treat the warning as a bug.
Tailwind CSS147 / 178
Module 17 · Components, reuse and @apply
@apply copies utilities into your own CSS
@apply lists utilities inside a CSS rule, and Tailwind pastes in their declarations. It is the right tool for HTML you can’t add classes to, such as a blog post rendered from Markdown.
You are back to naming things and keeping two files in sync
A second way to style the same thing
Overriding .btn with a utility needs care with layers
A component
Markup and classes in one place
Variants are just inputs
Plain utilities, no new CSS
QuizInteractive · disabled in the PDF
Which is the best use of @apply?
Which is the best use of @apply?
Tailwind CSS148 / 178
Module 17 · Components, reuse and @apply
@reference for styles in separate files
Some frameworks let a component have its own <style> block (Vue, Svelte, Astro) or a CSS module. Each is processed on its own, so @apply there doesn’t know your theme. @reference points at your main stylesheet to borrow its theme and utilities without copying its CSS into the output again.
Without @reference, text-espresso would be unknown here; with @import, all of Tailwind would be duplicated.
Tailwind CSS149 / 178
Module 17 · Components, reuse and @apply
Cascade layers decide before specificity does
Cascade layers group CSS into ranked buckets. When two rules conflict, the browser compares their layers first: the later layer wins, however specific the other selector is. Tailwind puts everything in four layers, in this order. CSS in no layer at all beats every layer.
Unlayered CSSPlain rules outside any @layer
@layer utilitiesTailwind’s classes, and your @utility ones
@layer componentsClass-based components, if you write any
@layer basePreflight and element styles (h1, a)
@layer themeTheme variables only
From strongest to weakest, for normal (not !important) declarations.
Predict the outputInteractive · disabled in the PDF
How much padding does <div class="note p-6"> get?
CSS
1@import"tailwindcss";23.note{4padding:12px;5}
How much padding does <div class="note p-6"> get?
Tailwind CSS150 / 178
Module 17 · Components, reuse and @apply
Writing into the right layer
src/style.css
1@import"tailwindcss";23@layer base {4h1{font-family:var(--font-display);}5a{text-underline-offset:3px;}6}78@layer components {9.recipeh2{@apply mt-8 text-2xl font-bold; }10}
base: defaults for plain elements, so a bare <h1> looks right.
components: styles for HTML you don’t control, or classes you really need.
Either way, a utility on the element still wins, because utilities is a later layer.
For long-form content. The official @tailwindcss/typography plugin (@plugin "@tailwindcss/typography";) adds a prose class that styles headings, lists, quotes and links inside rendered content, so you rarely need to write those rules yourself.
Tailwind CSS151 / 178
Module 17 · Components, reuse and @apply
Recap
Loops make repetition free; extract a component when copies in different files must change together.
For component variants, map to complete class names; never build them from pieces.
Class attribute order doesn’t decide conflicts; stylesheet order does. Don’t send conflicting classes.
@apply is for HTML you can’t put classes on, not a way to rename utilities.
@reference lets separate style blocks use your theme without duplicating CSS.
Layers rank before specificity: theme, base, components, utilities, then unlayered on top.
FlashcardsInteractive · disabled in the PDF
Why bg-${c}-100 fails
The scanner reads text and never sees the finished class name.
Which conflicting utility wins?
The one later in the generated CSS, not later in the attribute.
@apply
Copies utilities’ declarations into a CSS rule.
@reference
Borrows a stylesheet’s theme and utilities without emitting its CSS.
Cascade layer
A ranked bucket of CSS; a later layer beats an earlier one regardless of specificity.
Unlayered CSS
Beats every layer, including Tailwind’s utilities.
Module 18: Your own utilities, variants and sources
Tailwind CSS152 / 178
Module 18 · Your own utilities, variants and sources
@utility adds a class that behaves like Tailwind’s own
When a style you need has no utility, define one. A @utility lands in the utilities layer, works with every variant (hover:, md:, dark:), and is only written into the CSS when a file uses it.
CSS
1/* A horizontal strip of categories that scrolls,2 with the scrollbar hidden. */3@utility scrollbar-hidden {4scrollbar-width:none;5&::-webkit-scrollbar{6display:none;7}8}
Nesting with & works inside @utility.
Tailwind 3 habit
@layer utilities {.scrollbar-hidden{ … }}/* v4: always written, and no variants */
Tailwind 4
@utility scrollbar-hidden { …}/* written when used, with every variant */
QuizInteractive · disabled in the PDF
With the @utility above, does md:scrollbar-hidden work?
With the @utility above, does md:scrollbar-hidden work?
Tailwind CSS153 / 178
Module 18 · Your own utilities, variants and sources
Functional utilities take a value
A name ending in -* makes a family. Inside, --value() says which values are accepted, tried in order: a theme namespace, a bare type like integer, or an arbitrary value in brackets.
Lines 1-3: A theme token, in a namespace of your own.
Lines 5:tab-* matches tab- followed by anything.
Lines 6:tab-wide reads the token. tab-2 matches integer and writes 2. tab-[3] matches the bracket form. Anything else, such as tab-wide-ish, produces nothing.
Module 18 · Your own utilities, variants and sources
@custom-variant adds a condition
You met it in dark mode. The short form takes a selector, where & is the element. The block form can wrap anything, with @slot marking where the utility’s declarations go.
CSS
1/* Inside any part of the page marked data-brand="night" */2@custom-variant night (&:where([data-brand=night] *));34/* Hover, on any device that has a pointer able to hover */5@custom-variant any-hover {6@media(any-hover:hover){7&:hover{8@slot;9}10}11}
QuizInteractive · disabled in the PDF
What does @slot stand for in the block form?
What does @slot stand for in the block form?
Tailwind CSS155 / 178
Module 18 · Your own utilities, variants and sources
@variant uses a variant inside your own CSS
In plain CSS you can’t write dark:. @variant dark { … } lets a normal rule use any variant, built-in or custom, so your CSS stays in step with the utilities: if you redefine dark, this follows too.
CSS
1.receipt{2background:white;3@variant dark {4background:var(--color-stone-900);5}6}
With the default dark, this becomes a prefers-color-scheme media query.
Tailwind CSS156 / 178
Module 18 · Your own utilities, variants and sources
@source controls what gets scanned
Tailwind finds your files automatically. It skips files listed in .gitignore, the node_modules folder, images and other binary files, and CSS files. When a class lives somewhere it skips, such as a component library you installed, register that path.
Directive
Does
@source "../node_modules/@acme/menu-ui";
Also scan this folder
@source not "../src/legacy";
Never scan this folder
@source inline("bg-amber-100");
Always generate these classes
@import "tailwindcss" source("../src");
Start automatic scanning from this folder
@import "tailwindcss" source(none);
No automatic scanning: only @source paths
@source inline() takes brace patterns, so one line can list many classes: @source inline("{hover:,}bg-amber-{100..300..100}"); writes bg-amber-100, -200 and -300, each with and without hover:. Use it for classes that only appear in a database or CMS.
QuizInteractive · disabled in the PDF
You installed a package of menu components that uses Tailwind classes. Its components show up unstyled. Why, most likely?
You installed a package of menu components that uses Tailwind classes. Its components show up unstyled. Why, most likely?
Tailwind CSS157 / 178
Module 18 · Your own utilities, variants and sources
Plugins, prefixes and the old config file
Line
Does
@plugin "@tailwindcss/typography";
Loads a plugin, a package that adds utilities or variants
@import "tailwindcss" prefix(tw);
Every class needs a prefix: tw:flex, tw:hover:bg-amber-100
@config "../tailwind.config.js";
Loads a Tailwind 3 style JavaScript config, to ease an upgrade
A prefix is useful when Tailwind shares a page with other CSS whose class names might clash. It looks like a variant and always comes first. The theme variables get it too: --tw-color-amber-100.
Prefer CSS over plugins. Most of what plugins did in Tailwind 3 is now a few lines of @theme, @utility or @custom-variant. Reach for a JavaScript plugin only when you need logic CSS can’t express, or to share one with others.
Tailwind CSS158 / 178
Module 18 · Your own utilities, variants and sources
Practice: which tool?
QuizInteractive · disabled in the PDF
Order statuses come from a database as classes like bg-green-100 that never appear in your code. How do you make sure they exist?
Order statuses come from a database as classes like bg-green-100 that never appear in your code. How do you make sure they exist?
Put in orderInteractive · disabled in the PDF
Your CSS has --tab-size-wide: 8; in @theme and a @utility tab-* that reads it with --value(), and a code sample in your HTML has hover:tab-wide. Put what Tailwind does in order.
1.Split it at the colon into the variant hover and the utility tab-wide
2.Scan your files and find the candidate hover:tab-wide
3.Match tab-* and resolve wide with --value(--tab-size-*)
4.Wrap the declaration in the hover variant and write the rule
5.Read your CSS: the @theme token and the @utility tab-* definition
Tailwind CSS159 / 178
Module 18 · Your own utilities, variants and sources
Recap
@utility name { … } adds a utility that works with every variant and is emitted on use.
@utility name-* with --value() (and --modifier()) makes a family that reads the theme.
@custom-variant defines a condition: a selector, or a block with @slot.
@variant dark { … } uses a variant in plain CSS.
@source adds or excludes paths; @source inline() safelists classes.
@plugin loads plugins, prefix() namespaces classes, @config loads an old JS config.
FlashcardsInteractive · disabled in the PDF
@utility
Defines a custom utility in the utilities layer.
--value()
In a functional utility: which values the * may be (theme keys, types, brackets).
@slot
Where a utility’s declarations go inside a block-form custom variant.
@variant
Applies a variant to declarations in your own CSS.
@source inline()
Always generate the listed classes (a safelist).
source(none)
Turns off automatic scanning; only @source paths are read.
What automatic scanning skips
.gitignored files, node_modules, binary files and CSS files.
Module 19: How the engine works, and Tailwind at scale
Tailwind CSS160 / 178
Module 19 · How the engine works, and Tailwind at scale
From files to a stylesheet in five steps
Tailwind reads your stylesheet: the theme, custom utilities, variants and sources.
It reads project files as text and collects every token that could be a class: a candidate.
Each candidate is split into variants, a utility name, a value and a modifier. Ones that don’t parse are dropped.
Valid ones are matched against the theme and turned into CSS rules.
Rules are sorted, put in layers, and only the theme variables in use are written.
Tailwind 4’s engine is written partly in Rust, a fast systems language. Full builds take milliseconds to a second or two, and rebuilds while you work are usually near instant, because only changed files are rescanned and only new candidates are generated.
Tailwind CSS161 / 178
Module 19 · How the engine works, and Tailwind at scale
The scanner doesn’t understand your code, and doesn’t need to
The scanner doesn’t know HTML from JavaScript from Markdown. It splits text into class-shaped tokens and lets the parser throw away the ones that aren’t real utilities. That makes it fast and language-agnostic, with two consequences you already know: classes must be written in full, and anything that looks like a class becomes one.
PlaygroundInteractive · disabled in the PDF
TSX
1const shade ='sky'23exportdefaultfunctionBadges(){4return(5<divclassName="flex gap-2">6<spanclassName={`rounded-full px-3 py-1 bg-${shade}-200`}>7 Built from pieces8</span>9<spanclassName="rounded-full bg-rose-200 px-3 py-1">10 Written in full11</span>12</div>13)14}
The first badge has no background: bg-sky-200 never appears whole. Add a comment // bg-sky-200 above the component and it turns blue. Change shade to rose and it works too, by luck.
QuizInteractive · disabled in the PDF
A JavaScript file contains only the comment // later: try bg-rose-500 here. Does bg-rose-500 end up in the CSS?
A JavaScript file contains only the comment // later: try bg-rose-500 here. Does bg-rose-500 end up in the CSS?
Tailwind CSS162 / 178
Module 19 · How the engine works, and Tailwind at scale
How a candidate is read
Part
In md:hover:bg-amber-600/50!
Meaning
Variants
md:hover:
Conditions, applied left to right
Utility root
bg
Which property family
Value
amber-600
Looked up in the theme (--color-amber-600)
Modifier
/50
Opacity here; line height on text-*
Important
! at the end
Adds !important to every declaration
Watch out. The trailing ! is an escape hatch for fighting third-party CSS you can’t change. In your own code, a need for !important usually means unlayered CSS is beating your utilities: fix the layer instead.
QuizInteractive · disabled in the PDF
Which candidate produces no CSS with the default theme?
Which candidate produces no CSS with the default theme?
Tailwind CSS163 / 178
Module 19 · How the engine works, and Tailwind at scale
Why the generated order is predictable
Tailwind writes utilities in a fixed order, not in the order it found them. Within a layer, rules without variants come first; shorthands such as p-4 come before the longhands they overlap, such as px-2; and breakpoints come in ascending order of width, so a larger one wins. That is how p-4 px-2 reliably gives 1rem top and bottom, 0.5rem at the sides.
QuizInteractive · disabled in the PDF
What padding does <div class="px-2 p-4"> get?
What padding does <div class="px-2 p-4"> get?
Tip. The official Prettier plugin, prettier-plugin-tailwindcss, sorts the classes in your attributes into roughly the same order Tailwind writes them. Diffs get smaller and conflicts are easier to spot.
Tailwind CSS164 / 178
Module 19 · How the engine works, and Tailwind at scale
What ends up in the stylesheet
Only the utilities your files use, and only the theme variables those utilities (or your CSS) read.
Keyframes only for animations in use.
Small helper variables Tailwind uses internally (named --tw-*), registered with @property so they behave and animate well.
Preflight and your base styles, which are always included.
A full site’s CSS is typically tens of kilobytes before compression, and it stops growing once the common utilities are in use: the hundredth page reuses the same p-4 as the first. Utility CSS compresses very well because it is so repetitive.
QuizInteractive · disabled in the PDF
Which habit makes a Tailwind stylesheet grow the most?
Which habit makes a Tailwind stylesheet grow the most?
Tailwind CSS165 / 178
Module 19 · How the engine works, and Tailwind at scale
Upgrading code written for Tailwind 3
Run the official tool, npx @tailwindcss/upgrade, on a clean branch. It moves the JavaScript config into @theme, updates the imports and renames classes. Then review what changed. Tailwind 4 targets modern browsers (Safari 16.4, Chrome 111, Firefox 128 and later), because it relies on cascade layers, @property and color-mix().
Tailwind 3
Tailwind 4
@tailwind base; and friends
@import "tailwindcss";
tailwind.config.js theme
@theme { … } in CSS
shadow-sm, shadow
shadow-xs, shadow-sm
rounded-sm, rounded
rounded-xs, rounded-sm
outline-none
outline-hidden (outline-none now really removes it)
ring (3px, blue)
ring-3 and a colour; plain ring is 1px, currentColor
border (gray-200)
border is currentColor: add a colour
bg-opacity-50
bg-black/50
bg-[--brand]
bg-(--brand)
Find the bugInteractive · disabled in the PDF
After an upgrade, which line of this card still uses a Tailwind 3 class that no longer exists?
HTML
Tailwind CSS166 / 178
Module 19 · How the engine works, and Tailwind at scale
Scaling to a design system
When several sites or teams share a look, put the theme in one shared stylesheet, published as a package. Each app imports Tailwind and then the shared theme, and adds an @source for the shared component package so its classes are generated.
theme.css holds @theme, the custom variants and utilities. One change there restyles every app.
A review checklist for Tailwind codeInteractive · disabled in the PDF
Tailwind CSS167 / 178
Module 19 · How the engine works, and Tailwind at scale
Recap
Tailwind reads your CSS, scans files for candidates, parses, generates, sorts and writes.
The scanner reads all text, comments included, and never runs code.
A candidate is variants, a utility, a value, a modifier, and maybe !; invalid ones vanish silently.
Output order is fixed: shorthands before longhands, smaller breakpoints before larger.
CSS size follows how many different classes you use, not how often.
Upgrade with npx @tailwindcss/upgrade, then review renamed and changed defaults.
A design system is a shared theme stylesheet plus @source for shared components.
FlashcardsInteractive · disabled in the PDF
Candidate
A class-shaped token the scanner found, not yet known to be valid.
Trailing !
Makes a utility !important: bg-amber-600!.
p-4 px-2
px-2 wins at the sides: longhands are written after shorthands.
npx @tailwindcss/upgrade
The official tool for moving a project from Tailwind 3 to 4.
Tailwind 4’s browser floor
Safari 16.4, Chrome 111, Firefox 128.
prettier-plugin-tailwindcss
Sorts class attributes into a consistent order.
Module 20: Mastery challenge
Tailwind CSS168 / 178
Module 20 · Mastery challenge
How to take this module
Each slide is one problem drawn from several modules. Try to answer before looking anything up, and read every explanation, including for the ones you get right: they show the reasoning an experienced reviewer would use.
Tip. If you are unsure, build it. A Vite project with Tailwind takes a minute, and devtools will settle any question about which rule wins.
Tailwind CSS169 / 178
Module 20 · Mastery challenge
Which rule wins?
Predict the outputInteractive · disabled in the PDF
What colour is the text of <p class="price text-amber-700">?
A teammate says text-espresso “doesn’t work”. Each check below follows from the one before. Put them in order.
1.Inspect the element: there is no rule for .text-espresso at all
2.No rule means it was never generated: the class is written in full in a file Tailwind scans
3.So the scanner saw it: in @theme, the token is spelled --colour-espresso
4.Read which rule beats it, and fix that layer or class rather than adding !
5.Rename it --color-espresso: the rule now appears, but crossed out
Tailwind CSS174 / 178
Module 20 · Mastery challenge
Find the bug: statuses
Find the bugInteractive · disabled in the PDF
Order statuses show coloured pills. “Ready” works, but “Brewing” comes out grey instead of amber. Which line?
TSX
Tailwind CSS175 / 178
Module 20 · Mastery challenge
Accessibility review
QuizInteractive · disabled in the PDF
Which of these changes make a product card more accessible? Choose all that apply.
Which of these changes make a product card more accessible? Choose all that apply.
Tailwind CSS176 / 178
Module 20 · Mastery challenge
Build it: a billing switch
A pricing card lets people pick monthly or yearly billing. It mixes state, aria-pressed, and classes chosen as complete names. Two buttons with aria-pressed are a simple, accessible choice between two options.
Code challengeInteractive · disabled in the PDF
Make the switch work, starting on Monthly. The chosen button has aria-pressed="true" and the classes in on; the other has aria-pressed="false" and the classes in off. Both keep rounded-md px-3 py-1. The price reads 9 a month, or 90 a year once Yearly is chosen.
TSX
1import{ useState }from'react'23const on ='bg-stone-900 text-white'4const off ='bg-white text-stone-700'56exportdefaultfunctionBillingSwitch(){7// TODO: remember the choice; set aria-pressed, classes and price8return(9<divclassName="max-w-xs rounded-xl border border-stone-200 p-4">10<divclassName="inline-flex gap-1 rounded-lg bg-stone-100 p-1">11<buttontype="button"className="rounded-md px-3 py-1">12 Monthly13</button>14<buttontype="button"className="rounded-md px-3 py-1">15 Yearly16</button>17</div>18<pclassName="mt-4 text-2xl font-bold">9 a month</p>19</div>20)21}
Tailwind CSS177 / 178
Module 20 · Mastery challenge
Final project: the coffee shop’s home page
Build one page from scratch with everything in the course. Do it without copying earlier slides; look things up only when stuck.
Build itInteractive · disabled in the PDF
Tailwind CSS178 / 178
Module 20 · Mastery challenge
Recap
Layers before specificity before order: unlayered CSS beats every utility.
Breakpoints read the screen, container variants read the container: check both.
Name groups and peers when they nest.
@theme inline for tokens that must follow overrides of other variables.
No rule in devtools means generation; a crossed-out rule means the cascade.
Real elements, visible focus, more than colour, and respect for reduced motion.
FlashcardsInteractive · disabled in the PDF
First question when a class does nothing
Is there a rule for it in devtools at all?
Unlayered p { color: blue } vs text-amber-700
Blue: unlayered CSS beats the utilities layer.
@4xl: vs xl:
A container at least 56rem wide vs a screen at least 80rem wide.
Nested hover targets
Name them: group/row and group-hover/row:.
Locking the palette
--color-*: initial; then only your own colours.
Answer key
1. What a web page is made of · HTML is made of elements · Fill the blanksClose the paragraph so the browser knows where it ends.1: </p>A closing tag repeats the name with a slash in front: </p>. Without it the browser guesses where the paragraph stops, and it often guesses wrong.
1. What a web page is made of · Attributes add details, and class is the one CSS uses most · QuizHow many classes does this button have?B. Three: order, now and wideSpaces separate classes, so this is three labels. A single class name can never contain a space. Any element can have classes, buttons included.
1. What a web page is made of · Elements nest inside each other, like a family tree · QuizIn the tree above, which elements are siblings of the ul?B. h2, p and imgSiblings share a parent. h2, p, ul and img are all children of section. The li elements are children of the ul, and section is its parent.
1. What a web page is made of · How a browser goes from files to pixels · Put in orderPut the browser’s steps in order.1. Download the HTML file 2. Build the tree of elements 3. Read the CSS and match rules to elements 4. Work out sizes and positions 5. Paint the pixelsThe browser needs the elements before it can match styles to them, and it needs the styles before it knows how big things are. Painting comes last because colour and text need a place to go.
1. What a web page is made of · CSS can live in three places · QuizThe shop has ten pages and wants the same brown headings on all of them. Where should that CSS go?C. One stylesheet file that every page links toOne file means one place to change. With attributes or <style> elements you would edit ten pages to change one colour, and you would miss one. Tailwind follows this: it writes one stylesheet for the whole site.
2. How CSS decides what things look like · Selectors pick elements by tag, class or position · QuizWhich selector styles only the prices, written as <span class="price">3.50</span>?B. .priceA dot means “class”. Without it, price looks for a <price> tag, which doesn’t exist. #price looks for an id, not a class. span price looks for a <price> tag inside a span.
2. How CSS decides what things look like · When rules disagree, the cascade picks a winner · QuizWhat colour is <p class="note">?B. OrangeBoth selectors are the same, so specificity ties and order decides: the later rule wins. Values never mix; one declaration wins and the other is ignored. Devtools would show green crossed out.
2. How CSS decides what things look like · More precise selectors beat less precise ones · QuizWhat colour is <p class="note">?A. Green, because a class beats a tagOrder only breaks a tie. Here .note (one class) is more specific than p (one tag), so green wins even though p comes later. Tailwind keeps almost every utility at one class, so specificity rarely surprises you.
2. How CSS decides what things look like · Some properties pass from parent to child · QuizWhich of these does a child inherit from its parent by default?A. font-size; D. colorfont-size and color are about text, so they inherit. border and background-color belong to the parent’s own box. A child looks like it has the parent’s background only because its own background is transparent by default.
2. How CSS decides what things look like · Pseudo-classes style an element in a state · Fill the blanksMake links turn orange while the pointer is over them.1: :hover, 2: colora:hover matches a link only while it is hovered. Tailwind writes this kind of rule for you when you add a hover: prefix to a class, which you will learn in the states module.
2. How CSS decides what things look like · Media queries apply rules only on some screens · QuizOn a phone 390px wide, what is the menu’s font size?A. 16pxThe phone is narrower than 768px, so the media query is false and only the first rule applies. Tailwind’s responsive prefixes, such as md:, are media queries like this one.
3. Boxes, units and colours · Every element is a box with four layers · QuizThe menu card’s text touches its border. Which property gives the text room without moving the card?B. paddingPadding is inside the border, between it and the content. Margin is outside, so it would move the card away from its neighbours and leave the text exactly where it was.
3. Boxes, units and colours · box-sizing decides what width measures · QuizWith border-box, a box has width: 300px, padding: 20px and a 5px border. How wide is its content area?B. 250pxThe 300px includes everything. Take away 20px of padding and 5px of border on each side: 300 − 40 − 10 = 250px. With content-box the content would be 300px and the box 350px.
3. Boxes, units and colours · Block boxes stack, inline boxes flow with text · QuizYou set width: 120px on a <a> link, and nothing changes. Why?B. A link is inline by default, and inline boxes ignore widthInline boxes are sized by their text. Make the link inline-block or block (or put it in a flex container) and width works.
3. Boxes, units and colours · Vertical margins between blocks can merge · QuizA heading has margin-bottom: 24px and the paragraph below has margin-top: 16px. How big is the gap?B. 24pxVertical margins between blocks collapse into the larger of the two, 24px. Horizontal margins never collapse, and margins inside a flex or grid container don’t either. That is one reason Tailwind layouts prefer gap on the container to margins on the children.
3. Boxes, units and colours · Sizes have units: px, rem, em, % and viewport units · QuizWith the usual 16px root font size, how much is 1.5rem?B. 24px1.5 × 16px = 24px. Tailwind writes most sizes in rem, so p-6 is 1.5rem, which is 24px at default settings.
3. Boxes, units and colours · Colours can be written several ways · Fill the blanksMake a black that is 20% opaque.1: /, 2: 0.2The slash separates the colour from its alpha. Alpha runs from 0 (invisible) to 1 (solid), or 0% to 100%. Tailwind gives you this as a short suffix: bg-black/20.
3. Boxes, units and colours · CSS variables store a value under a name · QuizThe shop rebrands to a darker brown. With the CSS above, what do you change?B. Only --coffee in :rootEvery var(--coffee) reads the one value, so changing it in one place updates the heading and the button together. That idea, a named design decision kept in one place, is called a design token, and Tailwind’s theme is made of them.
4. Why utility classes · A class name reads as property, then value · QuizWhat does px-3 most likely do?B. Adds padding on the left and rightp is padding and x is the horizontal axis, left and right. py-3 would be top and bottom, p-3 all four sides. The number is a step on the spacing scale, not a count of pixels: px-3 is 0.75rem.
4. Why utility classes · Tailwind reads your files and writes only the CSS you use · QuizYour site never uses the class bg-pink-500. Is it in your final CSS?B. No, it only writes classes it found in your filesTailwind only generates what it finds, so a typical site’s CSS stays small however many utilities Tailwind knows. Write bg-pink-500 somewhere it scans, and the rule appears.
4. Why utility classes · Utilities are not the same as inline styles · QuizWhich of these can a style attribute not do?B. Change the colour only while the element is hoveredA style attribute has no selector, so it can’t say “only when hovered” or “only on wide screens”. It can set any property and read variables. Tailwind writes real rules, which can carry :hover and media queries.
4. Why utility classes · Practice: from CSS to utilities · Fill the blanksFill in the three utilities.1: p-4, 2: font-bold, 3: rounded-lgp-4 is four steps of the 0.25rem spacing scale, 1rem. font-bold is weight 700. rounded-lg reads the --radius-lg token. The order of classes in the attribute doesn’t change the result.
5. Installing Tailwind and your first styled page · Make a project with Vite · QuizWhat does the last command, npm install, do?B. Downloads the packages listed in package.json into the project’s node_modules folderpackage.json lists the packages the project needs; npm install downloads them into a node_modules folder inside the project. Node.js itself must already be installed for npm to run at all, and nothing is published anywhere.
5. Installing Tailwind and your first styled page · One line of CSS switches it on · Fill the blanksWrite the line that loads Tailwind into a stylesheet.1: @import, 2: tailwindcss@import is a normal CSS rule that pulls another stylesheet in. Tailwind’s plugin sees "tailwindcss" and replaces it with the CSS your project needs.
5. Installing Tailwind and your first styled page · Preflight wipes the browser’s default look · QuizYou write <h1>Menu</h1> with no classes in a Tailwind project. How does it look?B. Like normal paragraph textPreflight sets headings to inherit the size and weight of the text around them. That is deliberate: you choose the size with text-4xl and the weight with font-bold. The <h1> tag still matters, because it tells search engines and screen readers that this is the page’s title.
5. Installing Tailwind and your first styled page · When a class does nothing · Find the bugThe Mocha price should be bold and brown like the Latte one, but it isn’t bold. Which line is wrong?Line 7: <span class="font-bold text-amber-800">4.00</span>Classes are separated by spaces, never commas. The browser reads font-bold, (comma included) as one class, and no rule matches that name, so the price isn’t bold; text-amber-800 is a separate word and still works. A class with no rule is silently ignored: there is no error, which is why inspecting the element is the fastest check.
6. Spacing and sizing · One number drives every space · QuizHow much padding is p-10?B. 2.5rem (40px)10 × 0.25rem = 2.5rem, which is 40px at the default root size. The number is a count of spacing steps, never of pixels.
6. Spacing and sizing · Padding: all sides, one axis, or one side · QuizWhich gives 1rem on the left and right, and 0.5rem on top and bottom?B. px-4 py-2px-4 is 1rem left and right; py-2 is 0.5rem top and bottom. p-4 p-2 sets all sides twice, and only one value can win, so you would get even padding.
6. Spacing and sizing · Margin pushes things apart, and mx-auto centres · QuizYou add mx-auto to a div and it doesn’t move. What is most likely missing?A. A width smaller than its parent, such as max-w-mdA block fills its parent’s width by default, so there is no space left to share between the two auto margins. Give it a smaller width (w-80, max-w-md) and the leftover space is split evenly. text-center centres the text inside a box, not the box itself.
6. Spacing and sizing · Widths and heights · QuizWhich makes a 3rem square for a coffee-cup icon?B. size-12size-12 sets both width and height to 3rem. w-12 h-12 does the same in two classes. w-full would stretch it, and square-12 doesn’t exist, so it would produce nothing.
6. Spacing and sizing · max-w-* keeps things readable · Fill the blanksMake a column at most max-w-2xl wide, centred, with 1rem padding at the sides.1: max-w-2xl, 2: mx-auto, 3: px-4The max width caps the column, mx-auto centres it in the leftover space, and px-4 keeps text off the edge of a phone screen, where the column is as wide as the window.
6. Spacing and sizing · Square brackets for a value outside the scale · Find the bugOne of these classes produces no CSS. Which line?Line 3: <div class="w-[calc(100%-2rem)]">…</div>The spaces split it into three classes: w-[calc(100%, -, and 2rem)], none of them valid. Remove the spaces or write underscores: w-[calc(100%_-_2rem)]. Inside calc(), the browser needs spaces around + and -; Tailwind adds them when it writes calc from a class, so w-[calc(100%-2rem)] works too.
7. Typography · Text sizes come with a matching line height · QuizWhat does text-base/8 do?B. Font size base (1rem) with a line height of 2remOn a font size, the part after the slash is the line height, measured in spacing steps: 8 × 0.25rem = 2rem. On a colour, the slash means opacity instead (text-black/50). Same symbol, different meaning, depending on the utility.
7. Typography · Line height and letter spacing · Fill the blanksGive the paragraph a relaxed line height and slightly wide letter spacing.1: leading-relaxed, 2: tracking-wideleading-relaxed is 1.625 times the font size, a comfortable rhythm for long text. tracking-wide adds 0.025em between letters. Both are named steps from the theme, so they stay consistent across the site.
7. Typography · Colour and alignment · Quiztext- starts both text-lg and text-amber-700. How does Tailwind tell them apart?B. By the value: lg is a font size in the theme, amber-700 is a colourTailwind looks the value up in the theme: --text-lg is a size and --color-amber-700 is a colour, so each class writes the right property. That is also why you shouldn’t name a colour lg: text-lg would turn into that colour and lose its size.
7. Typography · Long text: truncating, clamping and wrapping well · QuizProduct cards in a grid show a description, and long ones push the cards out of line. Which keeps every description to two lines?B. line-clamp-2line-clamp-2 shows two lines and ends the second with an ellipsis. truncate would allow only one line. A fixed height with overflow-hidden cuts text off mid-line with no “…”, and breaks when the reader’s text size is larger.
7. Typography · Lists and links need their looks back · Find the bugThe steps for ordering ahead should be numbered, but no numbers appear. Which line is the problem?Line 2: <ol class="mt-2 list-decimal pl-5">An <ol> normally shows numbers, but Preflight sets list-style: none on lists. list-decimal brings the numbers back, and pl-5 (or list-inside) makes room for them, since Preflight removed the padding too.
8. Colour, borders, corners and shadows · The same colour names work on every property · QuizWhich pairing is easiest to read?B. bg-amber-50 text-amber-900Readability depends on contrast, the difference in lightness between text and background. Shades 50 and 900 are far apart; 400 and 500 are nearly the same, and white on 200 is pale on pale. Accessibility guidelines ask for a contrast ratio of at least 4.5:1 for normal text.
8. Colour, borders, corners and shadows · A slash sets a colour’s opacity · Fill the blanksDarken a photo with a black overlay at 40%, keeping the white headline on it solid.1: bg-black/40bg-black/40 fades just the background. Tailwind writes it with color-mix(), a CSS function that mixes the colour with transparency. opacity-40 would have faded the headline too.
8. Colour, borders, corners and shadows · Borders: width, side, style and colour · QuizIn Tailwind 4, what colour is a plain border with no colour class?B. The current text colourTailwind 4 leaves the border at currentColor, the element’s text colour, which is the browser’s own default. Tailwind 3 made it gray-200, so older code that relied on that now shows dark borders. Always pair border with a colour, such as border-stone-200.
8. Colour, borders, corners and shadows · Shadows lift things off the page · QuizWhat does shadow-amber-900/30 do on its own, with no shadow-md or similar?B. Nothing visible: it only sets the colour of a shadowA shadow colour class sets the colour; a size class like shadow-md draws the shadow. You need both. The same is true of rings: ring-amber-500 needs ring or ring-2.
8. Colour, borders, corners and shadows · Rings and outlines draw around without taking space · Find the bugThe selected size of cup should have a 2px amber ring, but the ring is the text colour and thin. Which line is wrong?Line 3: <button class="rounded-md border px-3 py-1 ring-2 ring-amber-500">A plain ring is 1px and currentColor in Tailwind 4 (it was 3px and blue in Tailwind 3). Say both the width and the colour: ring-2 ring-amber-500.
8. Colour, borders, corners and shadows · Try it: a card from a spec · Code challengeStyle the card. The <article> needs rounded-xl border border-stone-200 bg-white p-6 shadow-md. The heading needs text-xl font-bold text-stone-900. The price needs text-amber-700.One solution:
Each class is one decision from the spec, so you can check them off one by one. border alone draws the line and border-stone-200 colours it; without the colour, Tailwind 4 uses the text colour, which is far too dark for a card edge. The order of classes in the attribute doesn’t matter.
9. Layout with flexbox · A row or a column · QuizIn the code above, is the <h3> a flex item of the outer div?B. No, only direct children are; the <h3> is an item of the inner columnFlexbox only arranges direct children. The outer row has two items, the image and the inner div. The inner div is itself a flex column, and the <h3> and <p> are its items.
9. Layout with flexbox · items-* lines items up across the axis · QuizAn icon and the word “Cart” sit in a flex row, but the icon looks too high. Which class on the row fixes it?B. items-centerIn a row, the cross axis is vertical, and items-* aligns on the cross axis. justify-center would move both items to the middle of the row horizontally. text-center aligns text inside a box. It helps to say it as: justify along, items across.
9. Layout with flexbox · Let items wrap onto new lines · Fill the blanksMake the tags wrap, with 0.5rem between them.1: flex-wrap, 2: gap-2flex-wrap lets items start a new line instead of shrinking; gap-2 is 0.5rem between them, across and down. Margins on each tag would leave extra space at the edges; gap only puts space between items.
9. Layout with flexbox · Grow, shrink and basis share out the space · Find the bugThe product name should be cut off with “…”, but instead it pushes the price off the screen. Which line needs a fix?Line 2: <div class="min-w-0 flex-1">A flex item won’t shrink below the width of its content unless told to, because its minimum width is auto. The long name sets that minimum, so truncate has nothing to cut. min-w-0 lets the item shrink, and then the ellipsis appears. This is the most common flexbox surprise.
9. Layout with flexbox · Centring anything · Fill the blanksCentre the “Closed today” message horizontally and vertically in a full-height section.1: justify-center, 2: items-centerjustify-center centres along the main axis (across), items-center along the cross axis (down). min-h-dvh makes the section as tall as the screen so there is room to centre in. In a real attribute, either order of the classes works.
10. Grid and positioning · grid-cols-* sets equal columns · QuizIn the grid above, where does “Chai” go?B. The first column of a second rowYou defined three columns, so after three items the grid starts a new row automatically. Rows are created as needed; you only have to define them if you want fixed heights.
10. Grid and positioning · Items can span several cells · Fill the blanksMake the seasonal special take two columns and two rows of the product grid.1: col-span-2, 2: row-span-2The special covers a two-by-two block in the top-left; the other items flow into the free cells around it. col-span-* and row-span-* go on the item, not the container.
10. Grid and positioning · A grid that picks its own number of columns · QuizThe grid above is 50rem wide with a 1.5rem gap. Roughly how many columns does it get?B. ThreeEach column needs at least 14rem. Three columns and two gaps need 3 × 14 + 2 × 1.5 = 45rem, which fits; four need 60.5rem, which doesn’t. So three columns, each stretched with 1fr to share the spare 5rem.
10. Grid and positioning · Flex or grid? · QuizWhich suits a photo gallery where every photo must line up in neat rows and columns?B. grid grid-cols-4With flex-wrap, each row is laid out separately, so a short last row or uneven widths break the alignment. Grid defines the columns once for every row, so everything lines up.
10. Grid and positioning · absolute places an element over another · QuizRemove relative from the outer div. Where does the badge go?B. To the top-right of the nearest positioned ancestor, often the whole pageabsolute measures from the closest ancestor that has a position other than static. Without relative on the card, it keeps looking up the tree, and often ends up using the page itself, so the badge lands in the corner of the screen.
10. Grid and positioning · Sticky, fixed, and what sits on top · Find the bugThe header should stay at the top while the menu scrolls, but it scrolls away. Which line is wrong?Line 2: <header class="sticky top-0 z-10 bg-white">sticky needs to know where to stick. Without top-0 it has no threshold and behaves like normal flow. The bg-white matters too: without a background, the content scrolling underneath would show through.
11. Responsive design and container queries · Mobile first: plain classes for every size, prefixes from a width up · QuizWhat does sm:text-center do on a 360px phone?B. Nothing: sm: starts at 40rem (640px), wider than the phoneThe most common responsive mistake. sm: means “from 640px up”, not “on small screens”. To style phones, use the plain class; to style only phones, use max-sm:. Read prefixes as “at this width and wider”.
11. Responsive design and container queries · Showing and hiding by size · Fill the blanksHide the link list on phones and show it as a flex row from md; do the opposite for the menu button.1: hidden, 2: flex, 3: hiddenThe list is hidden by default and becomes flex from md up. The button shows by default and is hidden from md up. Writing the phone state first and overriding upward is mobile-first in action.
11. Responsive design and container queries · Ranges and one-off widths · QuizWhich class gives padding only on tablet-sized screens, from md up to lg?B. md:max-lg:p-6md: starts the range and max-lg: ends it, and stacking them means both must be true. md:lg: would mean “at least md and at least lg”, which is just lg:. md:p-6 lg:p-6 applies from md upward forever.
11. Responsive design and container queries · Container queries respond to the space a component gets · QuizThe same card appears in a 20rem sidebar and a 40rem main column, on a 1280px screen. With @md:flex-row, how does it look?B. Stacked in the sidebar, side by side in the main columnContainer sizes follow their own scale: @md is 28rem. The sidebar is narrower, the main column wider, so each card responds to its own space. With md:flex-row, a screen breakpoint, both would be side by side on any wide screen.
12. States: hover, focus and friends · Focus shows where the keyboard is · QuizA designer finds the browser’s focus outline ugly and asks you to add outline-none to every button. What is the better response?B. Replace it with a styled focus-visible: outline or ring that fits the designWithout a visible focus indicator, keyboard users can’t tell where they are on the page, and the site becomes unusable for them. focus-visible: lets you design a good-looking indicator that mouse users rarely see, so both needs are met.
12. States: hover, focus and friends · Pressed, disabled and other element states · QuizWhat does md:hover:bg-amber-700 mean?B. Hovered, and on a screen at least 48rem wideVariants stack, and all of them must be true. Tailwind nests the hover rule inside the md media query. You can stack as many as you need: dark:md:hover:….
12. States: hover, focus and friends · group: style a child when its parent is hovered · Fill the blanksShow the arrow only while the card is hovered, using a group named card.1: group/card, 2: group-hover/cardThe parent gets group/card and the child group-hover/card:. Tailwind writes a selector like .group\/card:hover .group-hover\/card\:opacity-100, matching the child whenever that ancestor is hovered.
12. States: hover, focus and friends · peer: style an element by a sibling’s state · Find the bugThe warning should appear when the email is invalid, but it never does. Which line is the problem?Line 2peer-invalid: uses the CSS sibling combinator ~, which only matches elements after the peer. Here the message comes first, so it can never match. Put the message after the input (and position it with flex order-* or grid if it must appear above).
12. States: hover, focus and friends · has-*, not-* and in-* · QuizMenu rows need a divider under each, except the last. Which class goes on each row?B. not-last:border-bnot-last: negates last:, so every row but the last gets a bottom border. border-b first:border-0 removes the line from the first row instead. divide-y on the parent is another way to get the same result.
12. States: hover, focus and friends · Styling from data-* and aria-* attributes · QuizA script sets aria-expanded="true" on the button when the menu opens. Which of the code’s classes take effect?B. Only aria-expanded:font-bold on the button; the icon doesn’t rotateLike hover:, aria-expanded: checks the attribute on the element the class is on. The icon has no aria-expanded of its own. Add group to the button and write group-aria-expanded:rotate-180 on the icon, and it follows its parent.
12. States: hover, focus and friends · Try it: a toggle button styled by its state · Code challengeMake the Favourite button a toggle. Keep a pressed state (starting false), set aria-pressed={pressed} on the button, and flip it on each click. Add aria-pressed:bg-amber-600 and aria-pressed:text-white to its classes.One solution:
The classes never change: aria-pressed:bg-amber-600 is always there, and the CSS only matches while the attribute is "true". React writes aria-pressed={false} as "false", which the variant ignores. Swapping classes by hand (pressed ? 'bg-amber-600' : '') works too, but then the look and the attribute are two things to keep in step.
13. Dark mode · dark: applies a utility in dark mode · QuizIn dark mode, what background does the <article> have?B. stone-800dark:bg-stone-800 wins over bg-white while dark mode is on, because the dark rule comes later in Tailwind’s CSS and both are one class. The body’s background is behind the article, but the article paints its own.
13. Dark mode · A toggle needs dark: to follow a class · Fill the blanksMake dark: follow a data-theme="dark" attribute instead of a class.1: dark, 2: *Same shape, different selector: the element itself has data-theme=dark, or any ancestor does ([data-theme=dark] *). Data attributes are handy when there are more than two themes, such as data-theme="sepia".
13. Dark mode · Practice: what happens on a visit · Put in orderA reader who chose dark last time comes back. Put what happens in order.1. The browser starts reading the <head> 2. The inline script reads theme from localStorage 3. It adds the dark class to <html> 4. The stylesheet’s dark: rules now match 5. The page is painted, already darkBecause the script runs in the <head>, before anything is painted, the class is in place by the time the browser draws. Put the same code at the end of the <body> and readers see a white flash first.
13. Dark mode · Variables switch every colour at once · Find the bugWith the CSS above, the page switches to dark, but one card stays white. Which line?Line 3: <article class="rounded-xl border bg-page p-6">Mocha</article>bg-white is a fixed colour that ignores the theme. The first card has no background, so the page shows through it. Once you use role-based colours, any raw palette colour is a sign of a missed spot. The theming module goes further with this idea.
14. Forms and accessibility · Every field needs a real label · QuizWhat connects a <label> to its <input>?B. The label’s for matching the input’s id, or the input sitting inside the labelEither for/id or wrapping (<label>Email <input></label>) creates the link. Position and classes mean nothing to a screen reader. If a design hides the label visually, keep it with sr-only rather than deleting it.
14. Forms and accessibility · Validation states without JavaScript · QuizA form loads with an empty required email field styled invalid:border-red-600. What does a new visitor see?B. A red border immediately, before typing anythingAn empty required field is invalid from the start, so invalid: matches straight away and greets people with an error. user-invalid: only matches after the person has changed the field or tried to submit.
14. Forms and accessibility · Try it: a field that shows its error · Code challengeWhen error is given, give the input border-red-600 instead of border-stone-300, set aria-invalid to true and aria-describedby="email-error", and show the error in <p id="email-error" className="mt-1 text-sm text-red-700">. Without an error, show no message.One solution:
Pick the whole class name in code (border-red-600 or border-stone-300), never part of one: Tailwind finds classes by reading your files, so it must see each full name. Keeping both borders and adding red on top would leave two classes setting one property, and the winner would depend on Tailwind’s CSS order, not yours. undefined leaves an attribute off entirely, which is what you want when there is no error.
14. Forms and accessibility · Checkboxes, radios and selects · Fill the blanksMake the checkbox amber, with the label clickable next to it.1: accent-amber-600accent-amber-600 sets accent-color, which recolours checkboxes, radios, range sliders and progress bars. Wrapping the input in the label makes the whole line clickable, with no for/id needed.
14. Forms and accessibility · Accessibility habits for every component · QuizA cart button shows only an icon. How do you give it a name a screen reader can say?B. <span class="sr-only">Cart</span>sr-only hides the text from the eye but keeps it in the page for screen readers, so the button is announced as “Cart, button”. hidden is display: none, which removes the text for assistive technology too. aria-hidden on the icon stops it being read as an unnamed image.
14. Forms and accessibility · Practice: review a sign-up form · Find the bugThis form looks fine with a mouse. One line makes it unusable for keyboard users. Which?Line 6: <button class="rounded-lg bg-amber-600 px-4 py-2 text-white">A <div> can’t be focused with Tab, isn’t announced as a button, and doesn’t submit the form when Enter is pressed. A <button> inside a form does all of that for free. Classes can make anything look like a button; only the element makes it work like one.
15. Transitions, transforms and animation · Transforms move, scale and rotate without disturbing the layout · QuizA product card gets hover:scale-105. What happens to the cards next to it?B. Nothing: the hovered card grows over them, the layout stays putTransforms only change how the element is drawn, not the space it takes. That is why they are smooth: the browser doesn’t have to recalculate the layout of the page on every frame. Changing width instead would push the neighbours around.
15. Transitions, transforms and animation · Animations run on their own · QuizWhich is the right tool for a button that darkens smoothly when hovered?B. A transition, transition-colorsHovering is a change between two states, which is exactly what a transition animates. An animation plays on its own whether or not anything changed, so animate-pulse would make the button throb forever.
15. Transitions, transforms and animation · Your own animation lives in the theme · Fill the blanksCreate an animate-steam utility for the steam above a coffee cup.1: --animate-steam, 2: steamThe variable name after --animate- becomes the class name: animate-steam. Its value is a normal CSS animation shorthand that names the keyframes, steam. infinite makes it loop.
15. Transitions, transforms and animation · Less motion for people who ask for it · Find the bugThe hero’s steam animation should only play for people who haven’t asked for reduced motion. Which line is wrong?Line 3: <span class="absolute top-6 motion-safe:animate-steam">~</span>With a plain animate-steam, the animation runs for everyone. motion-safe:animate-steam wraps it in @media (prefers-reduced-motion: no-preference), so it only plays when the person hasn’t asked for less motion.
15. Transitions, transforms and animation · Animate the cheap properties · QuizA cart drawer should slide in from the right. Which is smoothest?B. Transition translate-x-full to translate-x-0Translating moves the drawer without touching the layout, so each frame is cheap. Animating right or width forces layout work every frame, and changing width also squashes the content inside as it grows.
16. Theming with @theme and design tokens · The name’s prefix decides which utilities appear · QuizYou add --colour-latte: oklch(0.8 0.05 70); to @theme. What does bg-latte do?B. Nothing: the namespace is --color-, so no utility was createdTailwind only knows the namespaces it defines, spelled the American way. An unknown name is kept as a plain variable but makes no utilities, and bg-latte is silently ignored. Spelling namespaces exactly is part of the job.
16. Theming with @theme and design tokens · Extending the theme adds to the defaults · Fill the blanksAdd a text-hero size of 4.5rem with a tight line height of 1, and make the default rounded-lg a little larger.1: --text-hero, 2: --radius-lg--text-hero creates text-hero. A second variable with the double-dash suffix --line-height sets the line height that comes with it. Redefining --radius-lg changes every existing rounded-lg at once.
16. Theming with @theme and design tokens · Clearing a namespace replaces the defaults · QuizWith the theme above, a teammate writes text-stone-600. What happens?B. It produces no CSS: --color-stone-600 no longer existsAfter --color-*: initial, the only colours are the ones you list. Unknown classes are silently ignored, so the text stays its inherited colour. That is the point: off-palette colours can’t sneak in. The editor extension stops suggesting them too.
16. Theming with @theme and design tokens · Using tokens in your own CSS · Find the bugOnly the night brand should get a lighter espresso, but the whole site changes. Which line is wrong?Line 4@theme must be at the top level, because it defines which utilities exist for the whole site. Nested inside a selector it doesn’t become part of that selector: Tailwind still reads it as a theme value, so the new espresso lands on :root for everyone. To change a token in one part of the page, drop the @theme wrapper and set the variable in the rule itself: [data-brand="night"] { --color-espresso: …; }.
16. Theming with @theme and design tokens · Primitive, semantic and component tokens · QuizWhich class is the most future-proof for the error text under a form field?C. text-dangertext-danger says what the colour is for. If the brand changes its error colour, or dark mode needs a lighter one, you change one token. text-red-700 and the hex value bake today’s choice into every page.
17. Components, reuse and @apply · Variants of a component: map to complete class names · Find the bugThe badge colour comes from the data, but no badge ever has a background. Which line?Line 2: const bg = { amber: 'bg-amber-100', green: 'bg-green-100' }[colour]Tailwind reads files as text and never runs them. It sees bg-${colour}-100, which isn’t a class, so bg-amber-100 is never generated (unless it happens to appear elsewhere). Map each value to a complete class name written in the source.
17. Components, reuse and @apply · Try it: a button with kinds · Code challengeFill in kinds: secondary is bg-white text-stone-900 ring-1 ring-stone-300, danger is bg-red-600 text-white hover:bg-red-700. Then add kinds[kind] to the button’s classes, after rounded-lg px-4 py-2.One solution:
Each kind is a complete string written in the file, so Tailwind’s scanner finds every class. Building the name from pieces ('bg-' + colour + '-600') would compile and look right in code, but the class would never be generated. The space at the end of 'rounded-lg px-4 py-2 ' matters: without it, py-2 and bg-… run together into one class that doesn’t exist.
17. Components, reuse and @apply · When two classes set the same property · QuizA Button component has px-4, and someone uses it as <Button className="px-8">, which appends px-8. How much side padding does it get?B. Whichever of px-4 and px-8 Tailwind wrote later in its CSS: you can’t tell from the attributeClass attribute order doesn’t count; stylesheet order does. Tailwind’s own sort decides, which you shouldn’t rely on. Avoid sending conflicting classes: expose a prop such as size instead, or use a small library like tailwind-merge that removes the earlier conflicting class.
17. Components, reuse and @apply · @apply copies utilities into your own CSS · QuizWhich is the best use of @apply?B. Styling headings and links inside a blog post that arrives as HTML from a CMSYou can’t add classes to HTML a CMS sends, so CSS rules with @apply are the clean way to style it from your theme. For your own templates, a component keeps markup and styles together without inventing names.
17. Components, reuse and @apply · Cascade layers decide before specificity does · QuizHow much padding does <div class="note p-6"> get?B. 12px, from .note.note is in no layer, and unlayered styles beat every layer, including utilities. Both selectors are one class, so specificity would have tied, but layers are compared first. Put your own rules in @layer components so utilities can still override them.
18. Your own utilities, variants and sources · @utility adds a class that behaves like Tailwind’s own · QuizWith the @utility above, does md:scrollbar-hidden work?A. Yes, every variant works with a @utilityTailwind treats your utility exactly like its own, so it can be wrapped in any variant and is sorted with the rest. A plain class in @layer utilities gets none of that in Tailwind 4.
18. Your own utilities, variants and sources · Functional utilities take a value · Fill the blanksMake stripes-amber-200 read any theme colour.1: stripes-*, 2: --color-*The -* suffix makes it functional, and --value(--color-*) accepts any key in the colour namespace, so stripes-amber-200 becomes var(--color-amber-200). Add --modifier(integer) to read a value after a slash, such as stripes-amber-200/6.
18. Your own utilities, variants and sources · @custom-variant adds a condition · QuizWhat does @slot stand for in the block form?B. The declarations of whichever utility the variant is used withFor any-hover:underline, Tailwind writes the @media and &:hover from your definition and puts text-decoration-line: underline where @slot is. The same definition works with every utility.
18. Your own utilities, variants and sources · @source controls what gets scanned · QuizYou installed a package of menu components that uses Tailwind classes. Its components show up unstyled. Why, most likely?B. It lives in node_modules, which isn’t scanned: add an @source for itAutomatic detection skips node_modules because it is huge and mostly irrelevant. One @source line pointing at the package folder tells Tailwind to read it, and its classes are generated with yours from your theme.
18. Your own utilities, variants and sources · Practice: which tool? · QuizOrder statuses come from a database as classes like bg-green-100 that never appear in your code. How do you make sure they exist?B. @source inline(…) listing the classesClasses that only exist in data are invisible to the scanner. @source inline() safelists them so they are always generated. Better still, map statuses to classes in your code, where the scanner can see them.
18. Your own utilities, variants and sources · Practice: which tool? · Put in orderYour CSS has --tab-size-wide: 8; in @theme and a @utility tab-* that reads it with --value(), and a code sample in your HTML has hover:tab-wide. Put what Tailwind does in order.1. Read your CSS: the @theme token and the @utility tab-* definition 2. Scan your files and find the candidate hover:tab-wide 3. Split it at the colon into the variant hover and the utility tab-wide 4. Match tab-* and resolve wide with --value(--tab-size-*) 5. Wrap the declaration in the hover variant and write the ruleYour CSS comes first: it says what exists, and with @source, where to look. Only then does a class found in a file mean anything. Tailwind splits it, builds the utility from your definition and the token, then wraps it in the variant, which comes for free because tab-* is a real utility.
19. How the engine works, and Tailwind at scale · The scanner doesn’t understand your code, and doesn’t need to · QuizA JavaScript file contains only the comment // later: try bg-rose-500 here. Does bg-rose-500 end up in the CSS?B. Yes: the scanner reads all text, comments includedThe scanner doesn’t parse JavaScript, so it can’t tell a comment from code. Any scanned file contributes candidates, whether or not it is used. This is harmless (one extra rule) and a good reminder of how detection works.
19. How the engine works, and Tailwind at scale · How a candidate is read · QuizWhich candidate produces no CSS with the default theme?C. bg-amber-650There is no shade 650, so --color-amber-650 doesn’t exist and the candidate is dropped silently. hover:md:underline is valid: it is unusual to put the media query second, but Tailwind accepts it.
19. How the engine works, and Tailwind at scale · Why the generated order is predictable · QuizWhat padding does <div class="px-2 p-4"> get?B. 0.5rem left and right, 1rem top and bottomAttribute order doesn’t matter. Tailwind writes p-4 before px-2 because the shorthand sets more properties, so the more targeted px-2 comes later and wins for left and right. This is a deliberate, stable rule, unlike two utilities that set the same properties.
19. How the engine works, and Tailwind at scale · What ends up in the stylesheet · QuizWhich habit makes a Tailwind stylesheet grow the most?B. Safelisting every colour and shade with @source inline(), with and without several variantsReuse is free: a utility is written once however often it appears. Theme variables that are never used aren’t written. A broad safelist forces thousands of rules nobody may use. Keep safelists to exact classes you know you need.
19. How the engine works, and Tailwind at scale · Upgrading code written for Tailwind 3 · Find the bugAfter an upgrade, which line of this card still uses a Tailwind 3 class that no longer exists?Line 3: <div class="mt-4 bg-amber-600/75 px-3 py-1 text-white">The *-opacity-* utilities were removed in Tailwind 4; the slash modifier replaces them. shadow-sm on line 1 still exists, but it is now slightly larger than in Tailwind 3, which a reviewer should know to look for.
20. Mastery challenge · Which rule wins? · QuizWhat colour is the text of <p class="price text-amber-700">?C. Blue: unlayered CSS beats every layerLayers are compared before specificity. p { color: blue } is unlayered, so it beats both the components and the utilities layer, even though p is a weaker selector than a class. Moving that rule into @layer base would let the utility win again.
20. Mastery challenge · Read a responsive layout · QuizThe screen is 1000px wide and the page is full width. How many columns does the product grid have?C. Three1000px is 62.5rem. sm: (40rem) applies; xl: (80rem) doesn’t. @4xl: asks about the container: main is about 62.5rem wide, more than the 56rem it needs, so it applies too. Tailwind writes container variants after breakpoint variants, so @4xl:grid-cols-3 wins: three columns. That order also means that on a 1300px screen @4xl: would still beat xl:grid-cols-4, so avoid mixing the two kinds on one property.
20. Mastery challenge · Find the bug: nested groups · Find the bugHovering a menu row should show its “Add” button. Hovering anywhere in the menu currently shows every button. Which line?Line 5: <button class="opacity-0 group-hover/row:opacity-100">The button listens to the menu group, which is hovered whenever the pointer is anywhere in the section. It should listen to its own row, group-hover/row:. Naming groups exists precisely for this case. For keyboard users, also add group-focus-within/row:opacity-100 so the button appears when tabbed to.
20. Mastery challenge · Build a locked-down theme · Fill the blanksRemove the default colours, add two primitives, and make bg-surface follow a .dark override of the primitive.1: initial, 2: inline, 3: foam--color-*: initial clears the palette. @theme inline makes bg-surface write var(--color-foam) directly, so redefining --color-foam under .dark reaches it. With a plain @theme, --color-surface would be resolved on :root and ignore the override.
20. Mastery challenge · Debug a class that does nothing · Put in orderA teammate says text-espresso “doesn’t work”. Each check below follows from the one before. Put them in order.1. Inspect the element: there is no rule for .text-espresso at all 2. No rule means it was never generated: the class is written in full in a file Tailwind scans 3. So the scanner saw it: in @theme, the token is spelled --colour-espresso 4. Rename it --color-espresso: the rule now appears, but crossed out 5. Read which rule beats it, and fix that layer or class rather than adding !Devtools splits the problem in two at once: no rule means Tailwind never generated it (scanning or theme), a crossed-out rule means the cascade (layers, conflicts). Each check rules out one cause and points at the next. Reaching for ! first hides the real cause and makes the next override harder.
20. Mastery challenge · Find the bug: statuses · Find the bugOrder statuses show coloured pills. “Ready” works, but “Brewing” comes out grey instead of amber. Which line?Line 8: const classes = tone[state.trim().toLowerCase()] ?? 'bg-stone-100'The classes are all written in full, so this isn’t a scanner problem, which is the first thing to rule out. The API sends "Brewing " with a trailing space, the lookup misses, and the fallback bg-stone-100 is used. Trimming the input fixes it. Not every styling bug is a CSS bug.
20. Mastery challenge · Accessibility review · QuizWhich of these changes make a product card more accessible? Choose all that apply.A. Replace a clickable <div> with an <a href> around the card; C. Give the “Add to cart” icon button an sr-only label; E. Wrap the hover lift in motion-safe:A real link is focusable and announced correctly. A hidden label names the icon button. motion-safe: respects reduced-motion settings. Removing outlines strands keyboard users, and colour alone fails people who can’t tell red apart, so the word “Sale” (or an icon) is needed too.
20. Mastery challenge · Build it: a billing switch · Code challengeMake the switch work, starting on Monthly. The chosen button has aria-pressed="true" and the classes in on; the other has aria-pressed="false" and the classes in off. Both keep rounded-md px-3 py-1. The price reads 9 a month, or 90 a year once Yearly is chosen.One solution:
Solution
1import{ useState }from'react'23const on ='bg-stone-900 text-white'4const off ='bg-white text-stone-700'56exportdefaultfunctionBillingSwitch(){7const[yearly, setYearly]=useState(false)8constoption=(label:string, chosen:boolean, pick:boolean)=>(9<buttontype="button"aria-pressed={chosen}10onClick={()=>setYearly(pick)}11className={'rounded-md px-3 py-1 '+(chosen ? on : off)}>12{label}13</button>14)15return(16<divclassName="max-w-xs rounded-xl border border-stone-200 p-4">17<divclassName="inline-flex gap-1 rounded-lg bg-stone-100 p-1">18{option('Monthly',!yearly,false)}19{option('Yearly', yearly,true)}20</div>21<pclassName="mt-4 text-2xl font-bold">22{yearly ?'90 a year':'9 a month'}23</p>24</div>25)26}
One piece of state, yearly, drives everything: which button is pressed, which classes each gets, and the price. Keeping a separate “pressed” flag per button would let them disagree. The classes come from the complete strings on and off, so the scanner sees every one. With aria-pressed on both, a screen reader announces “Monthly, toggle button, pressed”; you could also style from it with aria-pressed:bg-stone-900 instead of choosing classes in code.