React does not come with its own styling system. A component produces HTML elements, and you style them the way you style any web page, with CSS. What React changes is where the styles live and how you attach them. This lesson shows the common approaches, from plain CSS to CSS Modules to inline styles, and how to choose.
Plain CSS and className
The simplest approach: write a .css file, import it in a component (Vite handles the import), and use className on elements. In the lesson editor you can write the CSS in a <style> element in the component:
function App() {
return (
<div>
<style>{`
.card { border: 1px solid #bbb; border-radius: 8px; padding: 12px; max-width: 260px; }
.card h3 { margin: 0 0 6px; }
.card .tag { background: #ffd45c; padding: 1px 8px; border-radius: 10px; font-size: 0.8rem; }
`}</style>
<div className="card">
<h3>React course</h3>
<p>Components, state and hooks.</p>
<span className="tag">New</span>
</div>
</div>
);
}
export default App;React course
Components, state and hooks.
NewIn a project you would put these rules in src/App.css and write import './App.css' at the top of App.jsx. It is ordinary CSS: selectors, media queries, pseudo-classes such as :hover, animations, everything you know. The CSS course covers it in depth.
The catch: plain CSS is global. A rule for .card in any imported file applies to every element with that class anywhere in the app, no matter which component it belongs to. In a big project two developers will each define .button or .title, and one will silently restyle the other's component. Teams avoid this with naming conventions such as BEM (card__title), or with scoping tools like CSS Modules.
CSS Modules: locally scoped class names
In a Vite project, any file ending with .module.css is a CSS Module. The build tool renames each class in it to a unique name and gives you an object that maps your names to the generated ones:
/* Button.module.css */
.button {
padding: 8px 14px;
border: 0;
border-radius: 6px;
background: #eee;
}
.primary {
background: #4b3df2;
color: white;
}import styles from './Button.module.css'
export default function Button({ variant = 'plain', children }) {
const className = variant === 'primary' ? `${styles.button} ${styles.primary}` : styles.button
return <button className={className}>{children}</button>
}I rendered <Button variant="primary">Save</Button> in a test, and the HTML that came out was:
<button class="_button_ba1d36 _primary_ba1d36">Save</button>.button became _button_ba1d36, with a short hash derived from the file. Another file's .button gets a different hash, so they can never clash. Use camelCase for class names with several words (.errorMessage is read as styles.errorMessage; styles['error-message'] also works). CSS Modules are the usual default in Vite and Next.js projects and need no extra tooling. Global styles (fonts, resets, variables) stay in a normal index.css; to target a class globally from inside a module, wrap it in :global(...).
Inline styles with the style prop
The style prop takes an object (you met it in the JSX lesson). It is best for values that come from data:
import { useState } from "react";
function ProgressBar({ percent, color = "#4b3df2" }) {
return (
<div style={{ background: "#eee", borderRadius: 6, height: 14, width: 220 }}>
<div style={{ width: percent + "%", height: "100%", background: color, borderRadius: 6, transition: "width 0.2s" }} />
</div>
);
}
function App() {
const [percent, setPercent] = useState(20);
return (
<div>
<ProgressBar percent={percent} />
<p>{percent}% complete</p>
<button onClick={() => setPercent(Math.min(100, percent + 30))}>Advance</button>
</div>
);
}
export default App;50% complete
AdvanceThe bar's width is the percentage, which is a different value on every render, so an inline style is the natural tool. For everything else inline styles are limited: they cannot express :hover, :focus, media queries, pseudo-elements or keyframe animations, they make markup noisy, and they have the highest specificity, which makes them hard to override. Use classes for the fixed look and inline styles for the dynamic bits.
CSS variables: the bridge between data and CSS
A neat compromise: set a CSS custom property with the style prop and use it in a stylesheet. The class holds the design (including hover and media queries), and the data only supplies a value:
function Swatch({ color, label }) {
return (
<span className="swatch" style={{ "--swatch": color }}>
{label}
</span>
);
}
function App() {
return (
<div>
<style>{`
.swatch { display: inline-block; padding: 6px 12px; margin: 4px; border-radius: 6px;
background: var(--swatch); color: #111; }
.swatch:hover { outline: 2px solid var(--swatch); outline-offset: 2px; }
`}</style>
<Swatch color="#ffd45c" label="Butter" />
<Swatch color="#86dbb4" label="Mint" />
<Swatch color="#bba8ff" label="Lilac" />
</div>
);
}
export default App;ButterMintLilacCustom property names go in the style object as strings ("--swatch"). The same trick drives dark mode, theming and design tokens: define variables once on :root (or on a themed wrapper) and use var(--name) everywhere.
Conditional classes
Components often need different classes depending on props or state. Build the string from the conditions. Three common ways:
function Tag({ active, size = "md", children }) {
// 1. A template string
const a = `tag tag-${size}${active ? " tag-active" : ""}`;
// 2. An array of parts, with falsy ones filtered out
const b = ["tag", "tag-" + size, active && "tag-active"].filter(Boolean).join(" ");
return <p>{children}: <code>{a}</code> / <code>{b}</code></p>;
}
function App() {
return (
<div>
<Tag active size="lg">Selected</Tag>
<Tag>Default</Tag>
</div>
);
}
export default App;Selected: tag tag-lg tag-active / tag tag-lg tag-active
Default: tag tag-md / tag tag-mdBoth produce the same class list. The third way is a tiny library, clsx (npm install clsx), that does exactly this job: clsx("tag", "tag-" + size, { "tag-active": active }). It accepts strings, objects (class name to boolean) and arrays, and drops anything falsy. With CSS Modules you combine it with the styles object: clsx(styles.tag, active && styles.active).
Other approaches
- Utility-first CSS (Tailwind CSS). Instead of writing CSS rules, you compose small single-purpose classes in the markup:
className="rounded bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700". It removes the naming problem and the stylesheets that grow forever, at the cost of long class lists. It is very popular with React teams and works with any component structure. It needs a build step, which Vite and Next.js support. - CSS-in-JS (styled-components, Emotion). Styles are written in JavaScript next to the component and generated at run time. They support dynamic styling well, but the run-time cost and the move towards server components have made them less common in new projects. Zero-runtime versions (vanilla-extract, Panda CSS, Linaria) extract the CSS at build time.
- Component libraries (MUI, Chakra UI, Mantine, shadcn/ui). Ready-made accessible components with their own styling systems. They speed up building an app, and are worth learning once you know the basics here.
- Sass/SCSS. Vite compiles
.scssfiles if you installsass; combine it with modules asButton.module.scss.
Choosing
| Situation | Suggestion |
|---|---|
| Learning, small projects | plain CSS file + className |
| Medium app, component-by-component styling | CSS Modules |
| Fast prototyping, a design system of utilities | Tailwind CSS |
| A value from data (width, colour, position) | style prop, or a CSS variable set with it |
| Theming and dark mode | CSS variables on :root |
| Pre-built UI, a quick admin panel | a component library |
Whatever you pick, stay consistent within a project, keep component styles close to the component, and keep global CSS small (fonts, variables, resets). Remember accessibility: sufficient colour contrast, visible focus outlines (do not remove outline without replacing it), and prefers-reduced-motion for animations.
Common mistakes
- Writing
classinstead ofclassName. - Putting global rules like
div { ... }or.title { ... }in a CSS file imported by one component, then being surprised that they affect the whole app. - Using inline styles for hover, media queries or animations, which do not work there.
- Forgetting
.module.cssin the file name, sostylesis undefined. - Using
styles.my-class(a hyphen is a minus); writestyles['my-class']or use camelCase. - Removing the focus outline with
outline: noneand no replacement. - Mixing three styling systems in one small project.
Exercise
- Turn the
cardexample into aCardcomponent with a CSS Module (in a project) and compare the class names in the browser's inspector. - Add a
variantprop toProgressBar("success","warning","danger") that picks a colour through a class instead of the inlinecolorprop. - Create light and dark themes with CSS variables on
:rootand adata-themeattribute toggled by a button. - Install
clsxin a project and rewrite the conditional class example with it.
Key points
- Ordinary CSS works in React: import a .css file and put class names on elements with className.
- Plain CSS is global, so class names can clash; CSS Modules (files ending in .module.css) give each class a unique generated name.
- Use the style prop only for values that come from data, such as a width or a colour from props; use classes and CSS variables for everything else.
- Build conditional classes with a template string, an array join or the clsx helper, and consider utility-first CSS such as Tailwind for larger teams.