Stop using someone else's design system. In this lecture, you make Tailwind truly yours — custom colors, custom fonts, and reusable components built from scratch.
This lecture takes you from Tailwind user to Tailwind power user:
@theme Directive — define your own brand colors and fonts that automatically generate Tailwind utility classes like bg-brand and font-display@apply Component Extraction — stop repeating 7-class combinations everywhere; turn them into clean, reusable class names like .menu-card and .btn-primaryfrom-*, via-*, and to-*dark: variant prefixRaw Tailwind defaults look generic. Every professional project has a brand — specific colors, typography, and repeating UI components. @theme and @apply are how you encode that brand into your workflow so every element you build looks cohesive, consistent, and yours. This is how real agency and product work gets done.
This lecture is the direct continuation of Lecture 6: Tailwind CSS Basics. You should already be comfortable with:
md: and lg:💡 You'll be building a complete Restaurant Digital Menu in this lecture — so you'll see every concept applied in a real, polished project, not just isolated examples.
📎 The cheatsheet is attached below the video. The full Restaurant Menu Component Library at the bottom is ready to copy directly into your own projects!
@theme ConfigUse this for: quick syntax lookup · copy-paste code · revision before the next class
How to use: scan section 1 for the big ideas. Use section 2 as a syntax reference. Grab code from section 3 when practicing. Check section 4 before submitting anything that feels off. If a term in the lecture confused you, section 5 has a plain-English translation. Section 6 points you to the assignment, quiz, and next lecture. Section 7 has interview questions with brief answers for self-testing.
Scope: this cheatsheet covers exactly what was taught in Lecture 7. Nothing extra. If it's not here, it wasn't in today's class.
styles.css that balloons toward 1,000 lines.bg-white rounded-lg shadow p-4; there is no pre-baked .card.@import "tailwindcss"; plus an optional @theme {} block, no JS config.content: [] glob.@theme {} is where your design system lives — custom tokens (--color-brand, --font-display) auto-become utilities (bg-brand, font-display).p-1 = 4px, p-2 = 8px, p-4 = 16px, p-8 = 32px; the constraint enforces visual rhythm.<color>-<intensity> — 50 lightest → 950 darkest, 500 is the canonical mid; Tailwind 4 ships richer P3 oklch() values.prettier-plugin-tailwindcss on day one.| File / line | Example | Purpose |
|---|---|---|
| Install command | npm install tailwindcss @tailwindcss/vite | Adds the engine + the official Vite plugin |
vite.config.js | plugins: [tailwindcss()] | Registers the Tailwind Vite plugin |
| CSS entry | @import "tailwindcss"; | Pulls in the entire framework (replaces v3's three @tailwind directives) |
@theme {} | @theme { --color-brand: #ff6b35; } | CSS-first config — replaces tailwind.config.js |
@theme token prefixes| Token prefix | Example | Becomes utility |
|---|---|---|
--color-* | --color-brand: #ff6b35; | bg-brand, text-brand, border-brand, ring-brand, ... |
--font-* | --font-display: "Inter", sans-serif; | font-display |
--spacing-* | --spacing-tight-1: 0.125rem; | p-tight-1, m-tight-1, ... |
| Family | Prefixes | Canonical example |
|---|---|---|
| Spacing 📏 | p-* m-* gap-* space-y-* | p-4 → padding: 16px; |
| Sizing 🔲 | w-* h-* max-w-* | max-w-sm → max-width: 24rem; |
| Color 🎨 | bg-* text-* border-* | bg-slate-900 → dark background |
| Typography 🅰 | text-* font-* leading-* tracking-* | text-3xl font-bold |
| Borders 🟦 | border-* rounded-* divide-* | rounded-lg → border-radius: 0.5rem; |
| Shadows ☁ | shadow-* ring-* | shadow-sm → subtle drop shadow |
| Class | Value | Class | Value |
|---|---|---|---|
p-1 | 4px | p-6 | 24px |
p-2 | 8px | p-8 | 32px |
p-3 | 12px | p-12 | 48px |
p-4 | 16px | p-[17px] | 17px (arbitrary escape hatch) |
| Intensity | Meaning |
|---|---|
50 / 100 / 200 | Lightest — backgrounds, subtle fills |
500 | Canonical "mid" hue |
700 / 900 / 950 | Darkest — text on light, dark surfaces |
| Safe contrast | Pair bg-*-900 with text-*-100 (never text-*-700) |
Every runnable snippet from the lecture, in slide order. Multi-file demos label each file above its block.
A full-page centered, padded, shadowed card built from utilities only — no <style> tag.
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1">6 <title>Compose, don't invent</title>7 <link rel="stylesheet" href="src/index.css">8</head>9<body>10 <div class="min-h-screen flex items-center justify-center bg-slate-100">11 <div class="bg-white p-6 rounded-lg shadow">12 Hello13 </div>14 </div>15</body>16</html>
Renders: a full-viewport light-slate background with one white card centered, 24px padding all around, 8px corner radius, subtle drop shadow. bg-white → background-color: rgb(255 255 255);, p-6 → padding: 1.5rem;, rounded-lg → border-radius: 0.5rem;.
The three-step install you'll repeat at the start of every project.
Terminal:
1# 1. Scaffold a fresh Vite vanilla project2npm create vite@latest tailwind-demo -- --template vanilla3cd tailwind-demo4npm install56# 2. Install Tailwind + the official Vite plugin7npm install tailwindcss @tailwindcss/vite89# 3. Start the dev server10npm run dev
vite.config.js:
1/* This block configures the Vite plugin.2 import { defineConfig } from 'vite'3 import tailwindcss from '@tailwindcss/vite'4 export default defineConfig({ plugins: [tailwindcss()] }) */
src/style.css — the entry stylesheet Vite injects:
1@import "tailwindcss";
index.html — confirm utilities work:
1<div class="min-h-screen flex items-center justify-center bg-blue-500">2 <div class="bg-white text-slate-900 p-8 rounded-2xl shadow-2xl">3 <h1 class="text-3xl font-bold">Tailwind is alive.</h1>4 </div>5</div>
Renders: npm run dev opens http://localhost:5173 — a blue full-viewport background with a white centered card, large bold heading "Tailwind is alive." Only ONE CSS file loads, containing only the utilities you used.
@theme block: brand color, font, spacingDeclare design tokens in CSS — they auto-become utilities.
src/style.css:
1@import "tailwindcss";23@theme {4 /* Brand color — becomes bg-brand, text-brand, border-brand, ring-brand, etc. */5 --color-brand: #ff6b35;6 --color-brand-soft: #ffb09a;78 /* Custom font stack — becomes font-display */9 --font-display: "Inter", system-ui, sans-serif;1011 /* Tighter spacing scale — overrides default 4px multiples for ts-* utilities */12 --spacing-tight-1: 0.125rem; /* 2px */13 --spacing-tight-2: 0.25rem; /* 4px */14}
Renders: typing <div class="bg-brand in HTML, IntelliSense autocompletes bg-brand and shows the #ff6b35 swatch. Applying bg-brand text-white p-4 font-display makes the card orange in the Inter font.
Use the tokens defined in @theme exactly like built-in utilities.
index.html — replace the existing card with this:
1<div class="min-h-screen flex items-center justify-center bg-slate-50">2 <div class="bg-brand text-white p-8 rounded-2xl shadow-xl font-display">3 <h1 class="text-3xl font-bold">Custom token = real utility</h1>4 <p class="mt-2 text-brand-soft">Tagline in the soft variant</p>5 </div>6</div>
Renders: a cream background with a centered solid-orange (#ff6b35) card, white heading in Inter, and a soft-orange (#ffb09a) tagline via text-brand-soft. bg-brand → background-color: #ff6b35; — a real CSS rule.
Each step is a multiple of 4px; arbitrary values are the rare exception.
1<div class="bg-slate-50 p-8">2 <div class="space-y-4">3 <div class="bg-blue-500 text-white p-1">p-1 → 4px padding</div>4 <div class="bg-blue-500 text-white p-2">p-2 → 8px padding</div>5 <div class="bg-blue-500 text-white p-4">p-4 → 16px padding</div>6 <div class="bg-blue-500 text-white p-8">p-8 → 32px padding</div>78 <!-- Escape hatch — only when the design genuinely demands it -->9 <div class="bg-rose-500 text-white p-[17px]">p-[17px] → 17px padding (escape hatch)</div>10 </div>11</div>
Renders: five stacked rectangles, each with visibly more inner padding than the one above. p-1 → padding: 0.25rem; (4px), p-2 → 0.5rem (8px); p-[17px] → literal padding: 17px;. The rose rectangle marks the off-scale example.
<color>-<intensity> predictabilitySame intensity scale across every color family.
1<div class="p-8 space-y-1 bg-slate-50">2 <h2 class="text-xl font-bold mb-2">Slate intensity scale</h2>3 <div class="bg-slate-50 p-2">slate-50</div>4 <div class="bg-slate-100 p-2">slate-100</div>5 <div class="bg-slate-300 p-2">slate-300</div>6 <div class="bg-slate-500 p-2 text-white">slate-500 (mid)</div>7 <div class="bg-slate-700 p-2 text-white">slate-700</div>8 <div class="bg-slate-900 p-2 text-white">slate-900</div>910 <h2 class="text-xl font-bold mb-2 mt-6">Same intensity, different colors</h2>11 <div class="bg-blue-500 text-white p-2">blue-500</div>12 <div class="bg-emerald-500 text-white p-2">emerald-500</div>13 <div class="bg-rose-500 text-white p-2">rose-500</div>14 <div class="bg-amber-500 text-white p-2">amber-500</div>15</div>
Renders: a slate column going near-white (top) to near-black (bottom), then four 500-intensity rows with consistent saturation across hues. bg-blue-500 → a real oklch() color; bg-slate-50 → oklch(0.984 0.003 247.858).
All six utility families on one realistic component, zero custom CSS.
1<body class="min-h-screen bg-slate-100 grid place-items-center p-8">2 <article class="max-w-sm w-full bg-white p-8 rounded-2xl shadow-xl3 text-slate-900 leading-relaxed4 border border-slate-100">5 <header>6 <h2 class="text-3xl font-bold tracking-tight">Pro Plan</h2>7 <p class="mt-1 text-sm font-medium text-slate-500 uppercase tracking-wider">8 Per month9 </p>10 </header>1112 <div class="mt-6 flex items-baseline gap-1">13 <span class="text-5xl font-bold">$29</span>14 <span class="text-slate-500">/ month</span>15 </div>1617 <ul class="mt-6 space-y-2 text-slate-700">18 <li>✓ Unlimited projects</li>19 <li>✓ Priority support</li>20 <li>✓ Custom domain</li>21 </ul>2223 <button class="mt-8 w-full bg-slate-900 text-white24 px-4 py-3 rounded-lg shadow-sm25 font-semibold">26 Choose Pro27 </button>28 </article>29</body>
Renders: a centered white pricing card on light-slate — deep shadow, rounded corners, generous padding, large bold "Pro Plan", uppercase slate-500 eyebrow, big "$29 / month", three checkmark rows, and a wide dark "Choose Pro" button. ~25 utilities, six families, zero custom CSS.
Same six families, smaller surface — a corner-pinned toast.
1<div class="fixed top-4 right-4 max-w-xs w-full2 bg-emerald-50 text-emerald-9003 border border-emerald-2004 px-4 py-3 rounded-lg shadow-sm5 text-sm font-medium">6 ✓ Order confirmed — see you soon!7</div>
Renders: a small toast pinned to the top-right corner — soft green background, deeper green text, thin emerald border, gentle drop shadow, reading "✓ Order confirmed — see you soon!"
Type a prefix and let IntelliSense reveal each sub-family.
1<!-- Type these one at a time, watch IntelliSense suggest -->2<h1 class="text-3xl font-bold tracking-tight">3 Autocomplete teaches the catalog4</h1>56<button class="px-4 py-2 rounded-md bg-blue-600 text-white shadow-sm">7 Click me8</button>
Renders: as you type each prefix (tex → text-*, font- → weights + families, rounded- → all radii), IntelliSense reveals the catalog organized by sub-family. Within ~30 seconds you have an idiomatic <h1> and <button> with no docs lookup.
prettier-plugin-tailwindcss: auto-sort utility orderCanonicalizes class order on save so code-review diffs stay clean.
Terminal:
1# 1. Install the plugin (run in terminal)2npm install -D prettier prettier-plugin-tailwindcss
.prettierrc — at the project root:
1/* JSON content of .prettierrc:2 { "plugins": ["prettier-plugin-tailwindcss"] } */
Before save (chaotic order) vs after save (canonical order):
1<!-- Before save (chaotic order) -->2<button class="text-white bg-blue-600 hover:bg-blue-700 px-4 rounded py-2">3 Click4</button>56<!-- After save (canonical order) -->7<button class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">8 Click9</button>
Renders: saving the file (Ctrl+S) reorders the class chain to the canonical sequence — layout → sizing → spacing → typography → color → shadows → state variants like hover:.
Find each utility in tailwindcss.com/docs using Cmd/Ctrl+K.
1<!-- 1. 16:9 aspect ratio -->2<div class="aspect-video">…</div>3<!-- aspect-video → aspect-ratio: 16 / 9; -->45<!-- 2. Stick to top of viewport -->6<header class="sticky top-0">…</header>7<!-- sticky → position: sticky; (paired with top-0 to anchor) -->89<!-- 3. 2px-wide blue focus ring -->10<button class="focus:ring-2 focus:ring-blue-500">…</button>11<!-- ring-2 → 2px ring; ring-blue-500 → blue color -->
Renders: aspect-video surfaces fastest; sticky also needs top-0 to anchor it; the focus ring uses the purpose-built ring-* family (not border-*).
.card") → fix: Tailwind ships primitives — compose bg-white rounded-lg shadow p-4.@apply only for system-wide patterns, never every button.@apply to keep HTML clean → fix: the right abstraction layer is a component, not a CSS class — @apply reintroduces naming pain.tailwind.config.js and expecting it to work in v4 → fix: most config moves into the @theme {} block; the content: [] field is gone.p-1 is 1px → fix: 1 means one unit on the 4px scale, so p-1 = 4px, p-2 = 8px, p-4 = 16px.p-[17px] because the design "looks right" → fix: stay on the 4px scale unless the design genuinely fails it — the constraint is the feature.--brand instead of --color-brand → fix: the --color- / --font- / --spacing- prefix is what turns a token into a utility.@theme {} block after first usage in the CSS → fix: keep @theme {} at the very top, right after @import "tailwindcss";.bg-*-500 with text-*-700 → fix: too-close intensities fail contrast; use bg-*-900 with text-*-100.text-base defensively everywhere → fix: text-base is the default; only specify a typography utility when overriding.padding, aspect-ratio); IntelliSense + repetition teach the top 50.| Term | Plain-English meaning |
|---|---|
| Utility class | A single-purpose CSS class — p-4 = padding: 16px;, bg-blue-500 = background-color: blue; |
| Utility-first CSS | A styling approach where you compose UIs from small single-purpose classes instead of authoring custom class names |
| Tailwind CSS | The most popular utility-first CSS framework. v4 (2025) is a major rewrite with CSS-first config |
| Vanilla CSS | Hand-written CSS in a .css file with custom class names (e.g., .card, .btn-primary) |
| Component library | Pre-built UI components shipped as a package — Bootstrap, MUI, Chakra UI |
| Design system | A consistent set of colors, spacing, typography, etc., used across an app — Tailwind enforces one through its constrained scales |
| Design token | A named design constant — e.g., --color-brand: #ff6b35. Tokens become utility classes in Tailwind 4 |
| Primitive | A single-purpose styling tool (one utility) — vs. a "component" which composes many primitives |
@import "tailwindcss" | The single CSS line in Tailwind 4 that pulls in the entire framework |
@theme {} | Tailwind 4's CSS-first configuration block — replaces the tailwind.config.js from v3 |
| JIT (Just-In-Time) | Tailwind's compilation strategy — only the utility classes you actually use end up in the production CSS |
| Auto content detection | Tailwind 4's automatic scan of source files for utility usage — replaces the v3 content: [...] glob |
| Vite | A modern build tool / dev server, faster than webpack — what Tailwind's official Vite plugin pairs with |
@tailwindcss/vite | The official Tailwind Vite plugin — registered in vite.config.js |
| Build step | The compile/transform process between authored source and shipped output — Tailwind needs one to work |
| Spacing scale | Tailwind's curated set of spacing values — every step is a multiple of 4px (1=4px, 2=8px, 4=16px, 8=32px) |
| Color intensity | The number after a color name — slate-50 (lightest) through slate-950 (darkest); 500 is the canonical "mid" |
oklch() | A perceptually uniform color space CSS supports — Tailwind 4 uses it for richer P3 wide-gamut colors |
| P3 wide gamut | A wider color space than sRGB — modern Macs/iPhones display P3, older displays fall back to sRGB |
| Arbitrary value | The […] escape-hatch syntax — p-[17px], bg-[#ff6b35]. Use sparingly |
max-w-prose | A typography utility set to ~65 characters per line — the readability sweet spot |
| IntelliSense | VS Code's autocomplete + hover-preview feature; Tailwind ships an official extension for utility autocomplete |
prettier-plugin-tailwindcss | The official Prettier plugin that auto-sorts utility class order on save |
| shadcn/ui | A component library that uses Tailwind under the hood — copy-paste components, not an npm dependency |
@apply | A Tailwind directive that lets you compose utilities into a custom class — use sparingly |
| Bootstrap / MUI / Chakra UI | Three popular component libraries (the non-Tailwind alternatives mentioned in Block 1) |
| TinyMCE | A rich-text editor often embedded in legacy CMSs (e.g., older WordPress); no build step = no Tailwind |
Practice: assignment.md — "Tailwind Profile Page Rebuild": rebuild your Lecture 1 personal profile page using only Tailwind utility classes (one stylesheet — src/index.css with @import "tailwindcss"; + an @theme {} block defining --color-brand and --font-display). Constraints: zero custom CSS, no Flexbox/Grid utilities, no responsive prefixes, no state variants (those are Lecture 8). Push to GitHub as a new repo named tailwind-profile. Submit before Lecture 8 (live URL by Friday 11:59 PM).
Quiz: quiz.md — open-book comprehension check. Do it before Lecture 8.
Next lecture: Lecture 8 — Tailwind Layout, Responsive Prefixes & State Variants. With the core utilities fluent, you'll layer on flex/grid layout, sm:/md:/lg: mobile-first responsive prefixes, and hover:/focus:/dark: state variants — one-line class strings replacing the media queries you wrote in vanilla CSS.
@theme directive, custom tokens, and overriding defaults.Stuck? Post in the class forum or DM the TA.
Quick-reference questions commonly asked about Lecture 7 concepts. Try answering each one before reading the brief answer.
What is utility-first CSS, and what trade-offs does it make compared to vanilla CSS or component libraries like Bootstrap and MUI? You compose UIs from small single-purpose classes instead of authoring custom class names, so there is no naming fatigue and the production bundle stays tiny. The trade-off is dense-looking markup (fixed by extracting components) versus vanilla CSS's unbounded growth and dead rules, or component libraries' faster start but prebuilt-vs-primitive philosophy and framework lock-in.
How does Tailwind 4's CSS-first @theme config differ from Tailwind 3's tailwind.config.js, and why is that change significant?
v4 replaces the four-file v3 dance (tailwind.config.js, postcss.config.js, @tailwind directives, content: []) with a single CSS file using @import "tailwindcss"; plus an optional @theme {} block. It matters because there is less ceremony and your design tokens now live next to the styles that consume them rather than in a separate JS file.
A teammate insists Tailwind produces bloated stylesheets in production. How do you defend or refute that claim using what you know about JIT compilation and auto content detection?
Refute it: Tailwind's JIT engine scans your source files (auto content detection — no content: [] glob needed) and emits only the utility classes you actually used. Real production bundles end up around 10–15 KB gzipped, far smaller than typical hand-rolled CSS at the same scale.
Walk me through setting up Tailwind 4 in a fresh Vite project — what files do you touch, and in what order?
Run npm install tailwindcss @tailwindcss/vite, then register tailwindcss() in the plugins array of vite.config.js, then add @import "tailwindcss"; at the top of your CSS entry file (and confirm that file is linked). Start the dev server and a utility class should light up live.
Why is the spacing scale (p-1, p-2, p-4, p-8) considered a feature, not a limitation? When is it correct to escape it with arbitrary values like p-[17px]?
The 4px scale forces every component to share the same spacing rhythm, so UIs stay consistent by default — the constraint is the design system. Use an arbitrary value like p-[17px] only when the design genuinely fails the scale; every arbitrary value is a hint you're fighting the system, so check if p-4 reads the same first.
What's the difference between extending Tailwind via @theme and writing custom CSS rules — when would you reach for each?
Extending via @theme adds a design token (e.g., --color-brand) that automatically becomes utilities (bg-brand, text-brand) and stays inside the constrained system. Writing custom CSS (or @apply) leaves that system and reintroduces naming pain, so reserve it for rare system-wide patterns; for everyday styling, extend tokens or compose primitives.
How do you handle the "class soup" critique of Tailwind in a code review?
Acknowledge the markup reads dense in raw HTML, but point out the fix is the right abstraction layer: extract the repeating utility chain into a component (the chain lives in one component file, not on every element). Reach for @apply only for system-wide patterns like typography resets — never to "fix" every button.