Skip to the content

Module 1: What a web page is made of

Module 2: How CSS decides what things look like

Module 3: Boxes, units and colours

Module 4: Why utility classes

Module 5: Installing Tailwind and your first styled page

Module 6: Spacing and sizing

Module 7: Typography

Module 8: Colour, borders, corners and shadows

Module 9: Layout with flexbox

Module 10: Grid and positioning

Module 11: Responsive design and container queries

Module 12: States: hover, focus and friends

Module 13: Dark mode

Module 14: Forms and accessibility

Module 15: Transitions, transforms and animation

Module 16: Theming with @theme and design tokens

Module 17: Components, reuse and @apply

Module 18: Your own utilities, variants and sources

Module 19: How the engine works, and Tailwind at scale

Module 20: Mastery challenge

Answer key

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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.
  22. 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.
  23. 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.
  24. 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.
  25. 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.
  26. 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.
  27. 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.
  28. 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.
  29. 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.
  30. 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.
  31. 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.
  32. 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.
  33. 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.
  34. 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.
  35. 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.
  36. 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.
  37. 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.
  38. 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.
  39. 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.
  40. 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.
  41. 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.
  42. 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.
  43. 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:
    Solution
    export default function OfferCard() {  return (    <article className="rounded-xl border border-stone-200 bg-white      p-6 shadow-md">      <h3 className="text-xl font-bold text-stone-900">        Seasonal blend      </h3>      <p className="text-amber-700">12.00 a bag</p>    </article>  )}
    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.
  44. 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.
  45. 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.
  46. 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.
  47. 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.
  48. 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.
  49. 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.
  50. 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.
  51. 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.
  52. 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.
  53. 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.
  54. 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.
  55. 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”.
  56. 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.
  57. 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.
  58. 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.
  59. 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.
  60. 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:….
  61. 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.
  62. 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).
  63. 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.
  64. 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.
  65. 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:
    Solution
    import { useState } from 'react'
    export default function FavouriteButton() {  const [pressed, setPressed] = useState(false)  return (    <button      type="button"      aria-pressed={pressed}      onClick={() => setPressed(!pressed)}      className="rounded-full border border-amber-600 px-4 py-2        text-amber-700 aria-pressed:bg-amber-600        aria-pressed:text-white"    >      Favourite    </button>  )}
    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.
  66. 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.
  67. 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".
  68. 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.
  69. 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.
  70. 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.
  71. 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.
  72. 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:
    Solution
    type Props = { error?: string }
    export function EmailField({ error }: Props) {  const border = error ? 'border-red-600' : 'border-stone-300'  const field = `mt-1 w-full rounded-lg border ${border} px-3 py-2`  return (    <div className="max-w-sm">      <label htmlFor="email" className="block text-sm font-medium">        Email      </label>      <input id="email" type="email"        aria-invalid={error ? true : undefined}        aria-describedby={error ? 'email-error' : undefined}        className={field} />      {error && (        <p id="email-error" className="mt-1 text-sm text-red-700">          {error}        </p>      )}    </div>  )}
    export default function Preview() {  return <EmailField error="That email is already registered." />}
    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.
  73. 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.
  74. 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.
  75. 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.
  76. 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.
  77. 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.
  78. 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.
  79. 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.
  80. 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.
  81. 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.
  82. 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.
  83. 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.
  84. 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: …; }.
  85. 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.
  86. 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.
  87. 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:
    Solution
    import type { ReactNode } from 'react'
    type Kind = 'primary' | 'secondary' | 'danger'type Props = { kind?: Kind; children: ReactNode }
    const kinds: Record<Kind, string> = {  primary: 'bg-amber-600 text-white hover:bg-amber-700',  secondary: 'bg-white text-stone-900 ring-1 ring-stone-300',  danger: 'bg-red-600 text-white hover:bg-red-700'}
    export function Button({ kind = 'primary', children }: Props) {  const classes = 'rounded-lg px-4 py-2 ' + kinds[kind]  return <button className={classes}>{children}</button>}
    export default function Preview() {  return (    <div className="flex gap-2">      <Button>Order</Button>      <Button kind="secondary">Details</Button>      <Button kind="danger">Cancel</Button>    </div>  )}
    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.
  88. 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.
  89. 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.
  90. 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.
  91. 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.
  92. 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.
  93. 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.
  94. 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.
  95. 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.
  96. 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.
  97. 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.
  98. 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.
  99. 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.
  100. 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.
  101. 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.
  102. 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.
  103. 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.
  104. 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.
  105. 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.
  106. 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.
  107. 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.
  108. 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.
  109. 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
    import { useState } from 'react'
    const on = 'bg-stone-900 text-white'const off = 'bg-white text-stone-700'
    export default function BillingSwitch() {  const [yearly, setYearly] = useState(false)  const option = (label: string, chosen: boolean, pick: boolean) => (    <button type="button" aria-pressed={chosen}      onClick={() => setYearly(pick)}      className={'rounded-md px-3 py-1 ' + (chosen ? on : off)}>      {label}    </button>  )  return (    <div className="max-w-xs rounded-xl border border-stone-200 p-4">      <div className="inline-flex gap-1 rounded-lg bg-stone-100 p-1">        {option('Monthly', !yearly, false)}        {option('Yearly', yearly, true)}      </div>      <p className="mt-4 text-2xl font-bold">        {yearly ? '90 a year' : '9 a month'}      </p>    </div>  )}
    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.