{"id":"l-rf-4-2","courseId":"react-fundamentals","lang":"en","kind":"lesson","summary":"Add Tailwind CSS v4 to the Vite project with its official plugin, style the Watchlist with utility classes and state variants, and customise the theme in CSS.","html":"<p>The Watchlist works, and it looks like 1998. You could write a stylesheet, and plenty of excellent apps do. This course uses <strong>Tailwind CSS</strong>, because it’s extremely common in React codebases you’ll join, and because styling right in your JSX keeps components self-contained: a <code dir=\"ltr\">MovieItem</code> file holds its markup, its behaviour and its look.</p>\n<p>Tailwind is a large set of small utility classes, each doing one thing: <code dir=\"ltr\">p-4</code> adds padding, <code dir=\"ltr\">rounded-lg</code> rounds corners, <code dir=\"ltr\">text-slate-600</code> sets a color. You compose them in <code dir=\"ltr\">className</code>. Tailwind scans your files, finds the classes you used, and generates a stylesheet containing only those.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Install it</h2>\n<p>Tailwind v4 has an official Vite plugin, and setup is three steps. Install the packages:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> tailwindcss</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @tailwindcss/vite</span></span></code></pre></div>\n<p>Add the plugin to your Vite config:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">vite.config.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { defineConfig } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'vite'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> react </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@vitejs/plugin-react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tailwindcss </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@tailwindcss/vite'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> defineConfig</span><span style=\"color:#953800;--shiki-dark:#FFA657\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  plugins: [</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">react</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">tailwindcss</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()]</span><span style=\"color:#953800;--shiki-dark:#FFA657\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">})</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>And import Tailwind at the top of your main stylesheet, which <code dir=\"ltr\">main.jsx</code> already imports:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/index.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"tailwindcss\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>That’s all. There’s no <code dir=\"ltr\">tailwind.config.js</code> and no PostCSS config in v4 by default. Tailwind finds your source files on its own. Vite restarts the dev server by itself when you save <code dir=\"ltr\">vite.config.js</code>; if the styles still don’t appear, stop it and run <code dir=\"ltr\">npm run dev</code> again.</p>\n<aside class=\"callout callout-note\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg><span><span class=\"callout-kind\">Note</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Older tutorials</span></p>\n<p>If you see <code dir=\"ltr\">@tailwind base; @tailwind components; @tailwind utilities;</code> or a <code dir=\"ltr\">content: [...]</code> array in a config file, that’s Tailwind v3. The ideas carry over, but the setup above is the current one.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Reading utility classes</h2>\n<p>Here’s the Watchlist shell:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/App.jsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mx-auto max-w-xl px-4 py-10\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h1</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"text-3xl font-bold tracking-tight text-slate-900\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Watchlist&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mt-1 text-sm text-slate-500\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">left</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> to watch&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  {</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* …form, filters, list */</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>The names are terse but systematic, and after a day you’ll read them as fast as CSS:</p>\n<ul>\n<li><strong>Spacing</strong> uses a scale where each step is 0.25rem: <code dir=\"ltr\">p-4</code> is 1rem of padding, <code dir=\"ltr\">mt-1</code> is 0.25rem of top margin, <code dir=\"ltr\">px-4</code> is horizontal padding. <code dir=\"ltr\">mx-auto</code> centres a block.</li>\n<li><strong>Sizes and type</strong>: <code dir=\"ltr\">max-w-xl</code> caps the width, <code dir=\"ltr\">text-3xl</code> sets the font size, <code dir=\"ltr\">font-bold</code> the weight.</li>\n<li><strong>Colors</strong> come from a palette with shades 50 to 950: <code dir=\"ltr\">text-slate-500</code>, <code dir=\"ltr\">bg-indigo-600</code>.</li>\n</ul>\n<p>Your editor can help: the official Tailwind CSS IntelliSense extension for VS Code autocompletes class names and shows the CSS each one produces on hover.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Variants: conditions in a prefix</h2>\n<p>Prefix a class to apply it only under a condition:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"jsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">src/components/MovieItem.jsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"flex items-center gap-3 rounded-lg border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-800\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'flex-1 text-slate-400 line-through'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'flex-1 text-slate-900 dark:text-slate-100'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.title</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    aria-pressed</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.watched</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    onClick</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onToggle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(movie.id)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"rounded-md border border-slate-300 px-3 py-1 text-sm hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-500 aria-pressed:border-indigo-600 aria-pressed:bg-indigo-600 aria-pressed:text-white\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">movie.watched </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Mark watched'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">li</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<ul>\n<li><code dir=\"ltr\">hover:</code> and <code dir=\"ltr\">focus-visible:</code> handle interaction states. Always give interactive elements a visible focus style for keyboard users.</li>\n<li><code dir=\"ltr\">dark:</code> applies when the user’s system is in dark mode (it follows <code dir=\"ltr\">prefers-color-scheme</code> by default).</li>\n<li><code dir=\"ltr\">aria-pressed:</code> applies when the element’s <code dir=\"ltr\">aria-pressed</code> is true. The toggle’s look follows the same attribute screen readers announce, so the two can’t disagree.</li>\n<li>Breakpoints are mobile-first: <code dir=\"ltr\">sm:</code>, <code dir=\"ltr\">md:</code> and <code dir=\"ltr\">lg:</code> apply from that width <strong>up</strong>, so write the phone layout first and add prefixes for larger screens.</li>\n</ul>\n<p>Conditions that depend on React state, like <code dir=\"ltr\">movie.watched</code> on the span, are a plain ternary choosing between two complete class strings.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Building class names from pieces</span></p>\n<p><code dir=\"ltr\">className={`text-${color}-600`}</code> looks clever and fails in production. Tailwind reads your files as text and only generates classes it finds written out in full; <code dir=\"ltr\">text-red-600</code> never appears, so it’s never generated. Map values to complete classes instead: <code dir=\"ltr\">const tone = { ok: 'text-green-600', error: 'text-red-600' }[status];</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Your own design tokens</h2>\n<p>Customise the theme in CSS with <code dir=\"ltr\">@theme</code>. Each variable becomes utilities automatically:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">src/index.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"tailwindcss\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@theme</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --color-brand-500</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.62</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.19</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 285</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --color-brand-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.55</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.21</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 285</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --font-display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Inter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">system-ui</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">sans-serif</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Now <code dir=\"ltr\">bg-brand-600</code>, <code dir=\"ltr\">text-brand-500</code> and <code dir=\"ltr\">font-display</code> work like built-in classes. Define a handful of tokens for your brand color and fonts rather than scattering one-off values like <code dir=\"ltr\">bg-[#6d28d9]</code> through your components.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">The trade-off</h2>\n<p>Long class lists are the honest cost. A styled button can carry fifteen classes. The answer isn’t to hide them in CSS with <code dir=\"ltr\">@apply</code>; it’s what you’ve practised all course: extract a component. A <code dir=\"ltr\">Button</code> component holds those fifteen classes in one place, and everywhere else you write <code dir=\"ltr\">&lt;Button&gt;</code>. Components are your unit of reuse in React, for styles as much as for behaviour.</p>\n<p>Tailwind isn’t the only good answer. CSS Modules (built into Vite: name a file <code dir=\"ltr\">MovieItem.module.css</code> and import it) give you scoped class names with ordinary CSS, and many teams prefer them. The skills transfer either way: semantic markup, a consistent spacing and color scale, visible focus states and a layout that works on a phone first.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Use the defaults first</span></p>\n<p>Tailwind’s spacing scale and palette are carefully designed. Sticking to them gives you a consistent look for free. Reach for custom values only when a design really requires them.</p>\n</aside>\n<p>The Watchlist looks like a real product now. One thing left: getting it onto the internet, which is the next lesson.</p>\n","toc":[{"id":"sec-1","text":"Install it","level":2},{"id":"sec-2","text":"Reading utility classes","level":2},{"id":"sec-3","text":"Variants: conditions in a prefix","level":2},{"id":"sec-4","text":"Your own design tokens","level":2},{"id":"sec-5","text":"The trade-off","level":2}],"takeaways":["Tailwind v4 needs <code dir=\"ltr\">npm install tailwindcss @tailwindcss/vite</code>, the plugin in vite.config.js, and <code dir=\"ltr\">@import &quot;tailwindcss&quot;;</code> in your CSS; no config file is required.","Utility classes map to single CSS declarations on a shared scale, such as <code dir=\"ltr\">p-4</code> for 1rem of padding and <code dir=\"ltr\">text-slate-600</code> for a palette color.","Prefixes apply a class conditionally, for example <code dir=\"ltr\">md:</code> above a breakpoint, <code dir=\"ltr\">hover:</code> on hover, <code dir=\"ltr\">dark:</code> in dark mode and <code dir=\"ltr\">aria-pressed:</code> when that attribute is true.","Tailwind finds classes by scanning your files as text, so always write complete class names and never build them with string interpolation.","Customise colors, fonts and other tokens with CSS variables in an <code dir=\"ltr\">@theme</code> block."],"further":[{"title":"Installing Tailwind CSS with Vite","url":"https://tailwindcss.com/docs/installation/using-vite"},{"title":"Styling with utility classes","url":"https://tailwindcss.com/docs/styling-with-utility-classes"},{"title":"Detecting classes in source files","url":"https://tailwindcss.com/docs/detecting-classes-in-source-files"},{"title":"Theme variables","url":"https://tailwindcss.com/docs/theme"}],"quiz":[{"q":"<p>You installed both packages and added <code dir=\"ltr\">@import &quot;tailwindcss&quot;;</code> to <code dir=\"ltr\">src/index.css</code>, but no utility classes have any effect. What’s the most likely missing step?</p>","options":[{"text":"Creating a <code dir=\"ltr\">tailwind.config.js</code> file.","why":"Tailwind v4 works without a config file; it detects your source files automatically."},{"text":"Adding a PostCSS config file.","why":"With Vite, the <code dir=\"ltr\">@tailwindcss/vite</code> plugin replaces the PostCSS setup. You don’t need both."},{"text":"Restarting your computer.","why":"Restarting the dev server can help after config changes, but the computer isn’t the issue."},{"text":"Adding <code dir=\"ltr\">tailwindcss()</code> from <code dir=\"ltr\">@tailwindcss/vite</code> to the <code dir=\"ltr\">plugins</code> array in vite.config.js.","why":"Correct. Without the plugin, Vite treats the import as plain CSS and never generates the utilities."}],"answer":3},{"q":"<p>A component builds a class as <code dir=\"ltr\">`text-${color}-600`</code> where <code dir=\"ltr\">color</code> is ‘red’ or ‘green’. In production the text has no color. Why?</p>","options":[{"text":"Tailwind scans files as plain text, sees no complete <code dir=\"ltr\">text-red-600</code> or <code dir=\"ltr\">text-green-600</code>, and never generates them.","why":"Correct. Map values to complete class names instead, for example <code dir=\"ltr\">{ red: 'text-red-600', green: 'text-green-600' }[color]</code>."},{"text":"Template literals aren’t allowed in <code dir=\"ltr\">className</code>.","why":"Any string works in className. The problem is that Tailwind can’t see the final class name when it builds the CSS."},{"text":"The 600 shade doesn’t exist for red and green.","why":"Every default palette color has shades from 50 to 950, including 600."},{"text":"Production builds strip all Tailwind classes that come from props.","why":"Tailwind doesn’t know about props. It only keeps classes that appear in full somewhere in your source files."}],"answer":0},{"q":"<p>What does <code dir=\"ltr\">className=&quot;bg-white md:bg-slate-100 dark:bg-slate-900&quot;</code> do?</p>","options":[{"text":"Applies all three backgrounds at once, and the last one wins.","why":"The prefixed classes only apply under their conditions; they don’t simply stack."},{"text":"White on small screens, slate-100 from the md breakpoint up, slate-900 in dark mode.","why":"Correct. Breakpoint prefixes are mobile-first (min-width), and <code dir=\"ltr\">dark:</code> follows the user’s color scheme preference by default."},{"text":"It’s invalid; you can only use one background class per element.","why":"You can combine as many classes as you need; variants exist precisely so that one element can have different styles in different conditions."}],"answer":1},{"q":"<p>Your filter buttons set <code dir=\"ltr\">aria-pressed</code>. What’s the cleanest way to style the active one with Tailwind?</p>","options":[{"text":"Compute the class in JavaScript with <code dir=\"ltr\">`bg-${active ? 'indigo' : 'white'}-600`</code>.","why":"That builds a class name dynamically, which Tailwind can’t detect. It’s also not needed here."},{"text":"Use <code dir=\"ltr\">style={{ background: … }}</code> because Tailwind can’t react to state.","why":"Tailwind has variants for exactly this. Inline styles also lose hover, focus and dark-mode handling."},{"text":"Use the <code dir=\"ltr\">aria-pressed:</code> variant, for example <code dir=\"ltr\">aria-pressed:bg-indigo-600 aria-pressed:text-white</code>.","why":"Correct. The styling follows the accessibility attribute you already set, so the visual state and what screen readers announce can’t disagree."}],"answer":2}],"exercise":{"kind":"fill","language":"js","template":"// Terminal\n// npm install {{0}} @tailwindcss/vite\n\n// vite.config.js\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tailwindcss from '{{1}}';\n\nexport default defineConfig({\n  plugins: [react(), {{2}}],\n});\n\n/* src/index.css */\n@import \"{{3}}\";\n\n// src/components/FilterButton.jsx: style the active filter\n<button aria-pressed={active} className=\"px-3 py-1 {{4}}:bg-indigo-600\">","blanks":[{"answers":["tailwindcss"]},{"answers":["@tailwindcss/vite"]},{"answers":["tailwindcss()"]},{"answers":["tailwindcss"]},{"answers":["aria-pressed"]}],"title":"Wire up Tailwind v4","prompt":"<p>Fill in the blanks to add Tailwind CSS v4 to the Watchlist’s Vite project, then style the active filter button using the attribute it already has.</p>\n","hints":["Two packages: the framework itself and its official Vite plugin.","The plugin is a function you call inside the <code dir=\"ltr\">plugins</code> array, next to <code dir=\"ltr\">react()</code>.","Tailwind has variants named after ARIA attributes. The button sets <code dir=\"ltr\">aria-pressed</code>."],"explanation":"<p><code dir=\"ltr\">npm install tailwindcss @tailwindcss/vite</code> adds the framework and its Vite plugin. The plugin is imported from <code dir=\"ltr\">@tailwindcss/vite</code> and called as <code dir=\"ltr\">tailwindcss()</code> in <code dir=\"ltr\">plugins</code>, and <code dir=\"ltr\">@import &quot;tailwindcss&quot;;</code> in your main CSS file pulls in the generated styles. No <code dir=\"ltr\">tailwind.config.js</code> is needed in v4. The <code dir=\"ltr\">aria-pressed:</code> variant applies <code dir=\"ltr\">bg-indigo-600</code> only when the button’s <code dir=\"ltr\">aria-pressed</code> is true, so the visual state follows the same attribute assistive technology reads.</p>\n","checkLabels":{}},"words":721,"readMinutes":3}