Styling components
Style React components with CSS files and className, CSS Modules, conditional classes, and style objects, and see what Tailwind and CSS-in-JS cost.
- 01Style a component with classesGive elements a className, write the rules in a CSS file, and keep two components' class names from clashing with CSS Modules.
- 02Pick classes and values from dataBuild a class string from props without stray words like false, and pass a value from data with a style object or a CSS variable.
- 03Compare the ways to styleSay when each way makes its CSS, what it needs installed, and how it fits React 19's stylesheets and Server Components.
A product card, styled from a CSS file
A React component decides what is on the screen. How it looks is still CSS's job, the same CSS you write for any web page. Here is a product card with its look in a separate CSS file. The example has two tabs, App.jsx and styles.css, and its result is live under the code:
export default function ProductCard() { return ( <article className="card"> <h3 className="card-title">Jasmine tea</h3> <p className="card-price">$4</p> <button className="card-button">Add to cart</button> </article> );}Lines 3 to 6 give each element a className, JSX's name for HTML's class attribute, as Writing markup with JSX explained. The styles.css tab has one rule per class: .card draws the orange border on line 4, .card-price makes the price orange and bold on lines 14 and 15, and .card-button colors the button. Press Edit & run, open styles.css, and change #c2410c on line 4 to teal: the border changes color as you type.
React's part is small. When it puts the <h3> on line 4 on the page, it writes <h3 class="card-title">. That's all. The browser then matches the rules in your stylesheet to the elements, exactly as it would for HTML you wrote by hand. React never reads your CSS.
className="card-title" into the page's class="card-title". The browser then finds the .card-title rule in styles.css and applies it. React never reads your CSS.How does styles.css reach the page? This preview links it for you. In a Vite project, a JavaScript file imports it, like the import "./index.css" in main.jsx that Setting up a React project showed, and Vite adds it to the page.
At a big event, the host pins a badge on each guest. A separate rule book says how people with each badge dress. The host doesn't need to know the rules, and the rule book doesn't care who pinned the badges.
- In real life: A badge pinned on each guest, like "staff"
- In React:
className="card-title"on an element - In real life: The rule book: "everyone with a staff badge wears blue"
- In React: The stylesheet:
.card-title { … } - In real life: The host pins badges and never reads the rule book
- In React: React writes
classand never reads your CSS - In real life: Two rule books that both mention "staff"
- In React: Two CSS files with the same class: the later rule wins
React puts class names and style attributes on elements; CSS does the styling. Every way to style React components is a choice about where you write that CSS, how its class names avoid clashing, and when it's made: before the app runs, or while it runs.
Plain CSS files apply to the whole page
A plain CSS file has no idea which component it was written for. Once it's on the page, its rules match every element on the page. That's fine until two files pick the same class name. This example has a ProductCard and a CartSummary, each with its own CSS file in its own tab:
function ProductCard() { return ( <article className="card"> <h3 className="title">Jasmine tea</h3> <p>$4</p> </article> );} function CartSummary() { return ( <aside className="summary"> <p className="title">Cart: 2 items</p> </aside> );} export default function App() { return ( <> <ProductCard /> <CartSummary /> </> );}Line 4 gives the product's heading className="title", and line 13 gives the cart's line the same class. In ProductCard.css, lines 7 to 9 make .title orange. In CartSummary.css, lines 5 to 8 make .title gray and capitals. Look at the result: JASMINE TEA is gray and in capitals too.
Both rules match both elements, because CSS is global. They have the same selector, .title, so the browser uses the one that comes later on the page, and CartSummary.css is linked after ProductCard.css. Press Edit & run and delete lines 5 to 8 of CartSummary.css: the product's title turns orange again, and so does the cart's line.
.title rules match both titles. They have the same selector, so the one linked later, CartSummary.css, wins: the product's title turns gray and capitalized too.Importing the CSS file from the component that uses it doesn't change this. In a Vite project, ProductCard.jsx would import its own file like this:
import "./ProductCard.css";
export default function ProductCard() {
return <h3 className="title">Jasmine tea</h3>;
}Line 1 imports the file for its side effect, with no name. It tells the build tool "this component needs these rules", so they load whenever ProductCard is used. But the rules still go on the page for everyone: the import doesn't tie .title to this component. (This block has no result: the lesson's previews link CSS files themselves, and the two do the same thing.)
Teams that stay with plain CSS avoid clashes with naming rules: every class starts with its component's name, like product-card-title and cart-summary-title. You'll try that in an exercise. It works, but only as long as everyone follows the rule. CSS Modules make the build do it for you.
CSS Modules: class names that stay in their file
A CSS Module is a CSS file whose name ends in .module.css. You write ordinary rules in it. When your build tool loads it, it renames every class to a name no other file uses, and gives your component an object that maps the names you wrote to the new ones. Vite and Next.js support CSS Modules with no setup.
Here is the product card's CSS as a module, unchanged except for its file name:
.card {
padding: 12px 16px;
border: 2px solid #c2410c;
border-radius: 12px;
}
.title {
color: #c2410c;
}import styles from "./ProductCard.module.css";
export default function ProductCard() {
return (
<article className={styles.card}>
<h3 className={styles.title}>Jasmine tea</h3>
<p>$4</p>
</article>
);
}Line 1 imports the module with a name, styles. It's a plain object: one property per class in the file. Line 5 reads styles.card and line 6 reads styles.title, so each element gets the new name of its class.
These two blocks have no live result. This site's previews link CSS files as they are and run no build step, so a .module.css file would stay global here, and its import would fail. To see what a module gives your component, here is the clash example from the last section with the build's work done by hand. Lines 2 and 3 are what each file's import styles would give it, and styles.css holds both files' rules after renaming:
// What the build gives each file for its import styles from "./….module.css"const productStyles = { card: "ProductCard_card_k3x9a", title: "ProductCard_title_k3x9a" };const summaryStyles = { summary: "CartSummary_summary_p7m2q", title: "CartSummary_title_p7m2q" }; function ProductCard() { return ( <article className={productStyles.card}> <h3 className={productStyles.title}>Jasmine tea</h3> <p>$4</p> </article> );} function CartSummary() { return ( <aside className={summaryStyles.summary}> <p className={summaryStyles.title}>Cart: 2 items</p> </aside> );} export default function App() { return ( <> <ProductCard /> <CartSummary /> </> );}Both components still say title in their code, on lines 8 and 17. But productStyles.title is ProductCard_title_k3x9a and summaryStyles.title is CartSummary_title_p7m2q, so the two rules in styles.css each match one element. The product's title is orange, and only the cart's line is gray. The names are made up for this lesson: Vite and Next.js each use their own form, with a short code that makes the name unique.
.title rules can't meet. Each file's styles.title holds its own new name. These names are made up for the picture; each tool picks its own form.Three details you'll meet in real projects:
- A class name with a dash, like
.card-title, is read with brackets:styles["card-title"]. Many teams write module classes in camelCase, like.cardTitle, so they can writestyles.cardTitle. - A class that isn't in the file is
undefined, and React writes noclassat all forclassName={undefined}. A typo gives you an unstyled element, not an error. - A rule for a tag name alone, like
h3orbutton, isn't renamed, so it still reaches every<h3>on the page in a Vite project. Next.js stops the build with an error instead, saying the selector isn't "pure". Either way, keep module rules on classes.
Choosing classes from props and data
Often a component's look depends on its data: a tea on sale gets an orange border, a sold-out tea fades. Write one class for each look in your CSS, and pick the classes in JavaScript. className takes one string, with the classes separated by spaces, so the job is to build that string:
const teas = [ { id: "jasmine", name: "Jasmine tea", price: 4, onSale: true, stock: 5 }, { id: "earl-grey", name: "Earl Grey", price: 3, onSale: false, stock: 0 }, { id: "mint", name: "Mint tea", price: 2, onSale: false, stock: 8 },]; function TeaCard({ tea }) { const classes = ["card"]; if (tea.onSale) classes.push("on-sale"); if (tea.stock === 0) classes.push("sold-out"); return ( <li className={classes.join(" ")}> <h3>{tea.name}</h3> <p>${tea.price}</p> </li> );} export default function App() { return ( <ul className="shelf"> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </ul> );}Line 8 starts an array of classes with "card", which every tea gets. Line 9 adds "on-sale" when the tea is on sale, and line 10 adds "sold-out" when its stock is 0. Line 13 joins the array into one string with a space between the words: "card on-sale" for Jasmine tea, "card sold-out" for Earl Grey, and "card" for Mint tea. In styles.css, .on-sale on lines 20 to 27 colors the border and adds " · Sale" after the name with ::after, and .sold-out on lines 29 to 32 fades the card and strikes it through. Change stock: 8 on line 4 to stock: 0, and Mint tea fades too.
A shorter way to write the same idea puts the condition right in a template literal, with the && operator that Conditional rendering used. It looks right, and it has a bug on purpose:
const teas = [ { id: "jasmine", name: "Jasmine tea", price: 4, onSale: true, stock: 5 }, { id: "earl-grey", name: "Earl Grey", price: 3, onSale: false, stock: 0 }, { id: "mint", name: "Mint tea", price: 2, onSale: false, stock: 8 },]; function TeaCard({ tea }) { return ( <li className={`card ${tea.onSale && "on-sale"}`}> <h3>{tea.name}</h3> <p>${tea.price}</p> </li> );} export default function App() { return ( <ul className="shelf"> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </ul> );}Line 9 writes `card ${tea.onSale && "on-sale"}`. For Jasmine tea, tea.onSale is true, and && gives "on-sale". For Earl Grey it's false, and && gives false. In JSX, React shows nothing for false, but a template literal isn't JSX: it turns false into the text "false". Earl Grey's card gets class="card false". Nothing looks wrong, because no rule matches .false. It stays a hidden bug until someone writes one, or until a test looks for an exact class.
See it for yourself. Pick a way to build the class string, switch the tea's data, and read the class attribute React really wrote:
function TeaCard({ name, onSale, soldOut }) { const className = `card ${onSale && "on-sale"} ${soldOut && "sold-out"}`; return <li className={className}>{name}</li>;}…- Earl Grey
Classes the browser sees: …
Pick a way to build the classes, and switch onSale and soldOut.
With && in a template and both switches off, the card gets class="card false false". ? : in a template gives "card " plus spaces, which the browser ignores: correct, if untidy. filter and join gives exactly "card", then "card on-sale", then "card on-sale sold-out", because filter(Boolean) drops every false value before join runs.
Many projects use a tiny library for this, clsx: clsx("card", onSale && "on-sale", soldOut && "sold-out") skips false values and joins the rest, like the array above. It's a convenience, not something React needs.
Style objects for values from your data
JavaScript in JSX with curly braces showed the style attribute: an object with camelCase properties, where a number on a size gets px. Classes and style objects aren't two equal ways to do the same thing. They're for different jobs. How to read the table: each row is something you might want, and whether each way can do it.
| What you want | A class and a CSS rule | A style object |
|---|---|---|
| A look that many elements share, like every card's border | Yes. Write the rule once and give each element the class. | Each element repeats the same object. |
A hover or focus look, like :hover | Yes, with :hover and :focus-visible in the rule. | No. A style object has no way to write :hover. |
| A different layout on narrow screens | Yes, with an @media rule. | No. A style object can't hold a media query. |
Extra content with ::after, like a Sale label | Yes, with ::after in the rule. | No. Pseudo-elements need a CSS rule. |
| A value you only know while the app runs, like a width from a percent | Only with a class for each possible value. | Yes. This is what style is for, alone or as a CSS variable. |
So the fixed look goes in CSS, and only the values from data go in style. React's documentation recommends the same: classes by default, and style only when a value depends on JavaScript.
You can have both at once with a CSS variable (a custom property, a name that starts with --). Set the variable with style, and let the stylesheet read it with var(). Then every rule can use the value from your data, including :hover:
const teas = [ { id: "matcha", name: "Matcha", color: "#15803d" }, { id: "hibiscus", name: "Hibiscus", color: "#be123c" }, { id: "chai", name: "Masala chai", color: "#b45309" },]; function TeaCard({ tea }) { return ( <li className="card" style={{ "--accent": tea.color }}> <h3>{tea.name}</h3> <button>Add to cart</button> </li> );} export default function App() { return ( <ul className="shelf"> {teas.map((tea) => ( <TeaCard key={tea.id} tea={tea} /> ))} </ul> );}Each tea on lines 2 to 4 has its own color. Line 9 sets one property, "--accent", to that color. Its name has dashes, so it's written in quotes, and React writes it as it is: style="--accent: #15803d;" for Matcha. In styles.css, line 11 draws the border in var(--accent), line 16 tints the background on hover, and line 27 colors the button. Hover over a card: each one tints in its own color, which a style object alone can't do. Add a fourth tea to the array with color: "#7c3aed", and it arrives with a purple border, button, and hover.
React 19: a component can bring its own stylesheet
Since React 19, a component can render the stylesheet it needs, right next to its markup. Give a <style> tag two props, href (a name for the stylesheet) and precedence (where it ranks against other stylesheets), and React treats it as a stylesheet for the whole page:
const badgeCss = `.sale-badge { margin-left: 6px; padding: 2px 8px; border-radius: 999px; background: #c2410c; color: white; font-size: 12px;}`; function SaleBadge({ children }) { return ( <> <style href="sale-badge" precedence="default">{badgeCss}</style> <span className="sale-badge">{children}</span> </> );} export default function App() { function countStyles() { const found = document.head.querySelectorAll("style[data-precedence]"); console.log(`<style> tags React put in <head>: ${found.length}`); } return ( <> <ul> <li>Jasmine tea <SaleBadge>20% off</SaleBadge></li> <li>Mint tea <SaleBadge>New</SaleBadge></li> <li>Masala chai <SaleBadge>Last 3</SaleBadge></li> </ul> <button onClick={countStyles}>Count the style tags</button> </> );}Lines 1 to 8 hold the badge's CSS in a string. SaleBadge returns two things in a Fragment: line 13 is the <style> with href="sale-badge" and precedence="default", and line 14 is the badge itself. Lines 28 to 30 use it three times, so React renders three <style> tags.
Press Count the style tags. The console under the result says <style> tags React put in <head>: 1. React moved the stylesheet out of the list and into the page's <head>, as <style data-href="sale-badge" data-precedence="default">, and kept one copy for the one href. Line 21 finds it by its data-precedence attribute. (The button's onClick runs countStyles when you click; Responding to events covers it.) Now delete precedence="default" from line 13 and press the button again: it counts 0. Without both props, React leaves each <style> where you wrote it, three copies inside the list. The badges still look the same, because a browser applies a <style> wherever it is.
precedence decides the order of stylesheets in <head>, and so which one wins a clash: React ranks the values in the order it first meets them, lower first. The same works for a stylesheet file with <link>:
function StoreMap() {
return (
<>
<link rel="stylesheet" href="/store-map.css" precedence="default" />
<div className="store-map">…</div>
</>
);
}Line 4 links store-map.css. With precedence, React puts the <link> in <head> once, however many maps render, and waits for the file to load before it shows the map, so it never appears unstyled. (This block has no result, because the preview has no store-map.css to load.) React notes three limits for these tags: it ignores changes to their props after the first render, it drops props other than href and precedence from a <style>, and it may keep the stylesheet after the component is gone.
You'll rarely write these tags by hand in an app: a project-wide CSS file is simpler. They matter most to libraries, as the next section shows. The Metadata, stylesheets and resource hints lesson comes back to stylesheets, with server rendering.
Tailwind and CSS-in-JS
Two more families are common in React projects. Both need a package installed, so this lesson shows what they do without depending on them.
Tailwind: many small classes
Tailwind CSS gives you a large set of utility classes: class names that each do one small thing, like px-4 for padding on the left and right, or font-bold. You style an element by listing them in its className, and you rarely write CSS yourself. Here is the overview's card, written that way:
export default function ProductCard() { return ( <article className="max-w-56 rounded-xl border-2 border-orange-700 px-4 py-3"> <h3 className="mb-1 font-bold">Jasmine tea</h3> <p className="mb-2 font-bold text-orange-700">$4</p> <button className="rounded-lg bg-orange-700 px-3 py-1.5 text-white hover:bg-orange-900"> Add to cart </button> </article> );}Line 3 gives the card a maximum width, rounded corners, a 2px orange border, and padding. Line 6 styles the button, and hover:bg-orange-900 darkens it while the mouse is over it. Hover over the button to see it. The utilities.css tab is a stand-in written for this lesson: simplified copies of the few rules Tailwind would write for these classes, because the preview doesn't run Tailwind. So only those classes work here: change px-4 on line 3 to px-8, and the padding disappears, because there's no rule for px-8 in the tab.
In a real project, that's the job Tailwind does while you build. Its build plugin scans your files for class names it knows and writes a rule only for each one it finds, so px-8 would simply work. The page gets one ordinary CSS file. To set it up in a Vite project with Tailwind 4 (the current major version, 4.3, when this lesson was checked in October 2026), you install tailwindcss and its Vite plugin, @tailwindcss/vite, add the plugin to vite.config.js, and put one line in your main CSS file:
/* src/index.css */
@import "tailwindcss";What Tailwind costs: long className strings, a set of class names to learn, and a build step. What it saves: naming classes, and clashes, because px-4 means the same everywhere. Building class strings from data works the same way as in the last sections, but write whole class names in your code, like onSale ? "border-orange-700" : "border-gray-400": the scan finds class names as text in your files, so a name glued together while the app runs, like `border-${color}-700`, gets no rule.
CSS-in-JS: styles written in JavaScript
CSS-in-JS libraries let you write CSS inside your component files, in JavaScript. The best known is styled-components, with Emotion close behind. Here is the card with styled-components:
import styled from "styled-components";
const Card = styled.article`
padding: 12px 16px;
border: 2px solid ${(props) => props.$accent};
border-radius: 12px;
`;
export default function ProductCard({ name, accent }) {
return (
<Card $accent={accent}>
<h3>{name}</h3>
</Card>
);
}Lines 3 to 7 make Card, a component that renders an <article> with the CSS between the backticks. Line 5 reads a prop, $accent, so each card's border can have its own color; the $ keeps the prop from reaching the page as an attribute. Lines 11 to 13 use Card like any component. (This block has no result, because the preview has no styled-components installed.)
When Card renders, the library builds the CSS text with the accent filled in, makes a class name from a short code of that text, adds a rule for it to the page if it isn't there yet, and gives the <article> that class. This is runtime CSS-in-JS: the CSS is made in the browser, while the app runs. To see that happen, here is a small model written for this lesson, not a real library, in 20 lines:
function hash(text) { let total = 0; for (const char of text) total = (total * 31 + char.charCodeAt(0)) % 1000000; return total.toString(36);} function css(text) { const name = "css-" + hash(text); return { name, rule: `.${name} { ${text} }` };} function Card({ accent, children }) { const style = css(`border: 2px solid ${accent}; border-radius: 12px; padding: 8px 12px; margin: 4px 0;`); return ( <> <style href={style.name} precedence="default">{style.rule}</style> <article className={style.name}>{children}</article> </> );} export default function App() { function showRules() { for (const tag of document.head.querySelectorAll("style[data-precedence]")) { console.log(tag.textContent); } } return ( <> <Card accent="#15803d">Matcha</Card> <Card accent="#be123c">Hibiscus</Card> <Card accent="#15803d">Sencha</Card> <button onClick={showRules}>Show the CSS rules</button> </> );}Lines 1 to 5 turn any text into a short code: the same text always gives the same code. Lines 7 to 10, css, make a class name from that code and a rule for it. On line 13 each Card works out its CSS with its own accent, and line 16 renders the rule as a React 19 <style>, with the class name as its href. Press Show the CSS rules. The console shows two rules, not three: Matcha and Sencha have the same accent, so the same CSS, the same class name, and one <style>. Change Sencha's accent on line 33, and a third rule appears.
Making CSS while the app runs has costs. The library's code downloads with your app and runs on every render. The browser has to work out its styles again each time a new rule arrives. React's documentation recommends against adding <style> tags while the app runs for these reasons, and suggests CSS files for fixed styles with style objects for the values from data.
There's a second cost with Server Components, which some frameworks, like Next.js, use: components that run on the server only, once, and send their result to the browser. They can't use context or effects, which runtime CSS-in-JS libraries long relied on to collect and add their rules. So in those apps, the libraries have needed a Client Component, a component marked to run in the browser too; Next.js's CSS-in-JS guide, checked in October 2026, still lists Emotion as working on support. React 19's <style href precedence> changed what's possible: since version 6.3 (January 2026), styled-components works in Server Components by rendering those tags, much like the model above. The Server Components lesson explains them.
A third family, zero-runtime CSS-in-JS, like vanilla-extract and StyleX, lets you write styles in JavaScript and turns them into a CSS file while you build, like CSS Modules. How to read the table: each row is one way to style, and each column asks the same question of all of them.
| Way to style | Where you write the CSS | Can class names clash? | What it needs | In Server Components |
|---|---|---|---|---|
Plain CSS files and className | In .css files, as ordinary rules. | Yes. Every class is global. | Nothing extra. Vite and frameworks load the files. | Yes. |
| CSS Modules | In .module.css files. | No. The build renames each file's classes. | Support in your build tool, which Vite and Next.js have. | Yes. |
| Style objects | On the element, in a JavaScript object. | No. They apply to one element. | Nothing extra. | Yes. |
| Tailwind CSS | As utility class names in your JSX. | Rarely. Each class does one small thing. | The Tailwind package and its build plugin. | Yes. It's ordinary CSS after the build. |
| Runtime CSS-in-JS, like styled-components or Emotion | In JavaScript, next to the component. | No. The library makes the class names. | The library, which runs in the browser. | Often only in Client Components. Newer versions of some, like styled-components 6.3 (January 2026), use React 19's <style>. |
| Zero-runtime CSS-in-JS, like vanilla-extract or StyleX | In JavaScript or TypeScript files. | No. The build makes the class names. | The library and its build plugin. | Yes. It's ordinary CSS after the build. |
None of these is the "React way": React only writes class and style. Teams pick what fits their project. A plain CSS file or CSS Modules need nothing extra, Tailwind trades CSS files for class lists, and CSS-in-JS keeps styles in JavaScript at a cost that depends on when it makes the CSS.
A plan picker that uses each tool for its job
A real component usually mixes the tools, each for what it's good at. Here is a plan picker for a subscription page:
const plans = [ { id: "starter", name: "Starter", price: 0, color: "#0f766e", soldOut: false }, { id: "plus", name: "Plus", price: 8, color: "#c2410c", soldOut: false }, { id: "team", name: "Team", price: 20, color: "#7c3aed", soldOut: true },];const selectedId = "plus"; function PlanCard({ plan, selected }) { const classes = ["plan"]; if (selected) classes.push("is-selected"); if (plan.soldOut) classes.push("is-sold-out"); return ( <li className={classes.join(" ")} style={{ "--accent": plan.color }}> <h3>{plan.name}</h3> <p>{plan.price === 0 ? "Free" : `$${plan.price} a month`}</p> </li> );} export default function App() { return ( <ul className="plans"> {plans.map((plan) => ( <PlanCard key={plan.id} plan={plan} selected={plan.id === selectedId} /> ))} </ul> );}- The CSS file holds the fixed look: the three columns, the border, the hover tint, and the
@mediarule that stacks the plans in one column when the result is narrower than 420px. - Classes for states: lines 9 to 11 add
is-selectedto the chosen plan andis-sold-outto Team, and line 14 joins them. The CSS gives the selected plan an outline, and the sold-out one a faded look with " · Sold out" after its price. - A CSS variable for data: line 14 also sets
--accentfrom each plan's color, so the border, outline, and hover tint follow the plan.
Change selectedId on line 6 to "starter": the outline moves to Starter, in teal. Nothing in the CSS changed; only the data did.
Sort each job into the tool that fits it:
- Rounded corners on every product card
- A progress bar's width from
percent - A darker background on hover
- One column on phones, three on wide screens
- The accent color the user picked in settings
- A tooltip's position, from where the mouse is
- A faded look when a product is sold out
- An avatar's size from
user.imageSize
Drag each styling job, or pick it and choose a group. Ask: is it a fixed look, or a value from data?
Common mix-ups
- "React has its own way of styling." No. React writes
classandstyleattributes. The browser's CSS does all the styling. - "Importing a CSS file into a component keeps its rules to that component." No. A plain CSS import adds the rules to the whole page. CSS Modules are what keep class names to one file.
- "Style objects are the React way to style." No. Classes are the default. A style object is for values from data, and it can't do
:hover, media queries, or::after. - "
`card ${onSale && "sale"}`adds nothing when onSale is false." It adds the wordfalse. In a template literal,falsebecomes text. Use a ternary, or filter an array. - "Tailwind puts all of its classes on the page." No. Its build writes rules only for the class names it finds in your files.
- "CSS-in-JS doesn't work with React 19." It depends on the library and on where the component runs. In the browser, they work. In Server Components, many need a Client Component, and some now use React 19's
<style>.
Practice exercises
Each exercise shows its result under the code, or when you press Edit & run. The CSS files are tabs you can edit too. For the first one, answer before you run anything.
soldOut is false. What class attribute does React write on the <li>?
const soldOut = false;
export default function TeaCard() {
return <li className={`card ${soldOut && "sold-out"}`}>Earl Grey</li>;
}const soldOut = false; export default function TeaCard() { return <li className={`card ${soldOut ? "sold-out" : ""}`}>Earl Grey</li>;}soldOut && "sold-out" gives false, and the template literal writes it as the text "false", so the <li> gets class="card false". The fix above uses a ternary, which gives an empty string instead.
Make the price orange and bold, with a class and a rule in the styles.css tab, not a style object.
export default function ProductCard() { return ( <article className="card"> <h3>Jasmine tea</h3> <p>$4</p> </article> );}export default function ProductCard() { return ( <article className="card"> <h3>Jasmine tea</h3> <p className="card-price">$4</p> </article> );}The <p> gets className="card-price", so React writes class="card-price", and the new .card-price rule in styles.css matches it: the price turns orange and bold. Any class name works, as long as the JSX and the CSS use the same one.
This is the clash from earlier: the product's title is gray. Rename the classes so each component's rules only reach its own elements. Then say what color the Jasmine tea title is.
function ProductCard() { return ( <article className="card"> <h3 className="title">Jasmine tea</h3> <p>$4</p> </article> );} function CartSummary() { return ( <aside className="summary"> <p className="title">Cart: 2 items</p> </aside> );} export default function App() { return ( <> <ProductCard /> <CartSummary /> </> );}function ProductCard() { return ( <article className="product-card"> <h3 className="product-card-title">Jasmine tea</h3> <p>$4</p> </article> );} function CartSummary() { return ( <aside className="cart-summary"> <p className="cart-summary-title">Cart: 2 items</p> </aside> );} export default function App() { return ( <> <ProductCard /> <CartSummary /> </> );}With a prefix from the component's name, each class matches one component only. ProductCard.css's .product-card-title keeps the product's title orange, and CartSummary.css's .cart-summary-title only styles the cart's line. CSS Modules do this renaming for you.
The settings tabs all look the same. Give the tab named by activeTab the extra class is-active. Then say what class attribute the Account tab gets.
const tabs = ["Profile", "Account", "Billing"];const activeTab = "Account"; export default function SettingsTabs() { return ( <ul className="tabs"> {tabs.map((tab) => ( <li key={tab} className="tab"> {tab} </li> ))} </ul> );}const tabs = ["Profile", "Account", "Billing"];const activeTab = "Account"; export default function SettingsTabs() { return ( <ul className="tabs"> {tabs.map((tab) => ( <li key={tab} className={tab === activeTab ? "tab is-active" : "tab"}> {tab} </li> ))} </ul> );}The ternary gives the Account tab "tab is-active" and the others "tab", so only Account gets the bold text and the orange line. A ternary never leaves a stray false.
The bar is empty. Make the green fill as wide as the order's progress, with the value from the data. Then say what width the fill gets.
const order = { item: "Masala chai", step: 3, steps: 4 }; export default function DeliveryProgress() { const percent = (order.step / order.steps) * 100; return ( <section> <p>{order.item}: step {order.step} of {order.steps}</p> <div className="track"> <div className="fill" /> </div> </section> );}const order = { item: "Masala chai", step: 3, steps: 4 }; export default function DeliveryProgress() { const percent = (order.step / order.steps) * 100; return ( <section> <p>{order.item}: step {order.step} of {order.steps}</p> <div className="track"> <div className="fill" style={{ width: `${percent}%` }} /> </div> </section> );}The width depends on the order, so it goes in a style object, and everything fixed stays in the CSS. Step 3 of 4 is 75, and the template literal makes "75%". Change step to 4 and the bar fills.
toastCss is written, but nothing puts it on the page, so the toasts have no border. Make Toast render it with React 19's <style>, then press Count the style tags. How many does it count?
const toastCss = `.toast { margin: 4px 0; padding: 6px 12px; border-left: 4px solid #0f766e; border-radius: 6px;}`; function Toast({ children }) { return <p className="toast">{children}</p>;} export default function App() { function countStyles() { const found = document.head.querySelectorAll("style[data-precedence]"); console.log(`<style> tags React put in <head>: ${found.length}`); } return ( <> <Toast>Saved your address.</Toast> <Toast>Matcha added to cart.</Toast> <Toast>Order placed.</Toast> <button onClick={countStyles}>Count the style tags</button> </> );}const toastCss = `.toast { margin: 4px 0; padding: 6px 12px; border-left: 4px solid #0f766e; border-radius: 6px;}`; function Toast({ children }) { return ( <> <style href="toast" precedence="default">{toastCss}</style> <p className="toast">{children}</p> </> );} export default function App() { function countStyles() { const found = document.head.querySelectorAll("style[data-precedence]"); console.log(`<style> tags React put in <head>: ${found.length}`); } return ( <> <Toast>Saved your address.</Toast> <Toast>Matcha added to cart.</Toast> <Toast>Order placed.</Toast> <button onClick={countStyles}>Count the style tags</button> </> );}With href and precedence, React 19 moves the <style> into <head> and keeps one per href. Three toasts render it three times, and the page gets it once: the button counts 1.
Check your understanding
For class questions, write down the string the code builds, word by word. Press Edit & run on a question's code to check your answer.
Question 1 of 7What class attribute does this Sencha card get?
Read the code, then predictconst isNew = true; const soldOut = false; export default function TeaCard() { const className = ["card", isNew && "new", soldOut && "sold-out"] .filter(Boolean) .join(" "); return <li className={className}>Sencha</li>; }Choose an answer to see the explanation.
Question 2 of 7Two plain CSS files both have a
.titlerule with the same selector, one for ProductCard and one for CartSummary. Which rule styles ProductCard's title?Choose an answer to see the explanation.
Question 3 of 7TeaCard.module.css has only a
.cardrule. What does this<li>get?Read the code, then predictJSXimport styles from "./TeaCard.module.css"; // TeaCard.module.css has one rule: .card { … } export default function TeaCard() { return <li className={styles.title}>Sencha</li>; }Choose an answer to see the explanation.
Question 4 of 7When is a style object the better choice than a class?
Choose an answer to see the explanation.
Question 5 of 7Three
SaleBadges each render<style href="sale-badge" precedence="default">. How many<style>tags does React 19 put in<head>?Choose an answer to see the explanation.
Question 6 of 7Why do many runtime CSS-in-JS libraries need a Client Component in an app that uses Server Components?
Choose an answer to see the explanation.
Question 7 of 7What makes Tailwind's
hover:bg-orange-900class change a button's color on hover?Choose an answer to see the explanation.
Key takeaways
- React writes
classNameas the page'sclass, andstyleas itsstyleattribute. The browser's CSS does the rest; React never reads it. - Plain CSS files are global, even when a component imports them. The same class in two files clashes, and the later rule wins.
- CSS Modules rename each
.module.cssfile's classes, and give you an object of the new names:styles.title. - Build class strings from data with a ternary, or an array with
filter(Boolean).join(" "). A template literal with&&writes the wordfalse. - Use
stylefor values from data, often as a CSS variable the stylesheet reads. Hover, media queries, and::afterneed CSS rules. - In React 19,
<style>and<link rel="stylesheet">withhrefandprecedencego in<head>, once perhref. - Tailwind writes rules for the utility classes it finds while you build. Runtime CSS-in-JS makes CSS in the browser, which costs time and often needs a Client Component with Server Components.
Remember the one-liner.
React only writes class and style; you choose where the CSS lives and when it's made.
Coming next: Responding to events. It starts the Adding interactivity stage: event handlers, passing them as props, and how a click travels through the tree.