cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

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.

By the end, you can
  • 01
    Style 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.
  • 02
    Pick 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.
  • 03
    Compare 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:

A product card styled with classNamePop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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.

Top left, the JSX h3 with className="card-title". Top right, the CSS rule .card-title from styles.css. Arrows from both lead down to a page window holding the h3 with class="card-title", which React wrote and the rule matches.App.jsx<h3 className="card-title">styles.css.card-title { … }React writesthe browser matchespage<h3 class="card-title">
React turns 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.

Real-life analogyA dress code by badge

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 class and 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
The main idea

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:

Two components, two CSS files, one class name (a clash on purpose)Pop out in the code editor (opens in a new tab)JSX
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 />    </>  );}
ResultLoading…

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.

Left, two CSS files. ProductCard.css has .title with color #c2410c, crossed out. CartSummary.css, linked later, has .title with color gray. Arrows lead from the gray rule to both titles on the page window: JASMINE TEA and CART: 2 ITEMS, both gray.ProductCard.css.title { color: #c2410c }CartSummary.css.title { color: gray }linked later, so it winspageJASMINE TEACART: 2 ITEMS
Both files are global, so both .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:

ProductCard.jsx imports its CSS file in a Vite projectJSX
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:

ProductCard.module.cssCSS
.card {
  padding: 12px 16px;
  border: 2px solid #c2410c;
  border-radius: 12px;
}

.title {
  color: #c2410c;
}
ProductCard.jsx reads its classes from the moduleJSX
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:

The clash example with CSS Modules, renamed by handPop out in the code editor (opens in a new tab)JSX
// 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 />    </>  );}
ResultLoading…

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.

Left, two .title cards, one from ProductCard.module.css and one from CartSummary.module.css. Arrows lead to a build card in the middle. From it, arrows lead to two different names: ProductCard_title_k3x9a and CartSummary_title_p7m2q.ProductCard.module.css.titleCartSummary.module.css.titlebuildProductCard's styles.titleProductCard_title_k3x9aCartSummary's styles.titleCartSummary_title_p7m2q
The build renames each file's classes, so the two .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 write styles.cardTitle.
  • A class that isn't in the file is undefined, and React writes no class at all for className={undefined}. A typo gives you an unstyled element, not an error.
  • A rule for a tag name alone, like h3 or button, 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:

A shelf of teas, with classes picked from each tea's dataPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

&& inside a template literal (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

Playground: which classes did React write?
TeaCard with && in a templateJSX
function TeaCard({ name, onSale, soldOut }) {  const className = `card ${onSale && "on-sale"} ${soldOut && "sold-out"}`;  return <li className={className}>{name}</li>;}
On the page…
  • Earl Grey

Classes the browser sees: …

Try it yourself
&& in a template

Pick a way to build the classes, and switch onSale and soldOut.

React renders this TeaCard on the page with the way and the data you pick. The class attribute shown is read from the real <li> after each render. Reset goes back to && with both false.

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.

A class or a style object?
What you wantA class and a CSS ruleA style object
A look that many elements share, like every card's borderYes. Write the rule once and give each element the class.Each element repeats the same object.
A hover or focus look, like :hoverYes, with :hover and :focus-visible in the rule.No. A style object has no way to write :hover.
A different layout on narrow screensYes, with an @media rule.No. A style object can't hold a media query.
Extra content with ::after, like a Sale labelYes, 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 percentOnly 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:

Each tea's color from data, as a CSS variablePop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

A SaleBadge that brings its own <style>Pop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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>:

A component that needs a stylesheet file, with <link>JSX
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:

The product card with utility classesPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…

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:

Tailwind 4 in your main CSS fileCSS
/* 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:

A card with styled-components (needs the library)JSX
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:

A tiny CSS-in-JS, written for this lessonPop out in the code editor (opens in a new tab)JSX
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>    </>  );}
ResultLoading…

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.

Left, before the app runs: four sources, styles.css, Card.module.css, Tailwind classes, and vanilla-extract, lead into one CSS file. Right, while the app runs in the browser: a Card component renders, then a style tag is added to the page, by runtime CSS-in-JS.Before the app runsstyles.cssCard.module.cssTailwind classesvanilla-extractCSSbuild toolsWhile the app runs<Card> renders<style> addedruntime CSS-in-JS,in the browser
CSS files, CSS Modules, Tailwind, and zero-runtime CSS-in-JS all become ordinary CSS before your app runs. Runtime CSS-in-JS works out the CSS in the browser, each time a component with new styles renders.

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.

The ways to style React components
Way to styleWhere you write the CSSCan class names clash?What it needsIn Server Components
Plain CSS files and classNameIn .css files, as ordinary rules.Yes. Every class is global.Nothing extra. Vite and frameworks load the files.Yes.
CSS ModulesIn .module.css files.No. The build renames each file's classes.Support in your build tool, which Vite and Next.js have.Yes.
Style objectsOn the element, in a JavaScript object.No. They apply to one element.Nothing extra.Yes.
Tailwind CSSAs 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 EmotionIn 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 StyleXIn 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:

A plan picker: a CSS file, classes for states, a variable for colorPop out in the code editor (opens in a new tab)JSX
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>  );}
ResultLoading…
  • The CSS file holds the fixed look: the three columns, the border, the hover tint, and the @media rule that stacks the plans in one column when the result is narrower than 420px.
  • Classes for states: lines 9 to 11 add is-selected to the chosen plan and is-sold-out to 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 --accent from 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:

A class or a style object?
  • 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
Try it yourself
0 of 8 correct

Drag each styling job, or pick it and choose a group. Ask: is it a fixed look, or a value from data?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "React has its own way of styling." No. React writes class and style attributes. 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 word false. In a template literal, false becomes 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.

Exercise 1 · Warm-upPredict the class attribute

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>;
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upGive the price a look of its own

    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>  );}
    ResultLoading…
      Exercise 3 · PracticeStop two titles from clashing

      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 />    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMark the active tab

        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>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFill a delivery bar from the data

          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>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeA toast that brings its own stylesheet

            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>    </>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The styling components quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What class attribute does this Sencha card get?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                const 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.

              2. Question 2 of 7Two plain CSS files both have a .title rule with the same selector, one for ProductCard and one for CartSummary. Which rule styles ProductCard's title?

                Choose an answer to see the explanation.

              3. Question 3 of 7TeaCard.module.css has only a .card rule. What does this <li> get?

                Read the code, then predictJSX
                import 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.

              4. Question 4 of 7When is a style object the better choice than a class?

                Choose an answer to see the explanation.

              5. 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.

              6. 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.

              7. Question 7 of 7What makes Tailwind's hover:bg-orange-900 class change a button's color on hover?

                Choose an answer to see the explanation.

              Key takeaways

              • React writes className as the page's class, and style as its style attribute. 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.css file'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 word false.
              • Use style for values from data, often as a CSS variable the stylesheet reads. Hover, media queries, and ::after need CSS rules.
              • In React 19, <style> and <link rel="stylesheet"> with href and precedence go in <head>, once per href.
              • 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.

              CompleteFrontend Clear concepts. Working examples.