Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 32 of 38

Styling React apps

Style React apps with plain CSS and className, CSS Modules, inline styles and CSS variables, build conditional classes, and compare Tailwind and CSS-in-JS.

18 minBuilding an appUpdated 24 October 2026

By the end of this lessonChoose a styling approach for a React project, apply classes conditionally, and avoid class-name clashes with CSS Modules.

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:

jsx
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;
result
React course
Components, state and hooks.
New

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

css
/* Button.module.css */
.button {
  padding: 8px 14px;
  border: 0;
  border-radius: 6px;
  background: #eee;
}

.primary {
  background: #4b3df2;
  color: white;
}
jsx
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:

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

jsx
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;
result
50% complete
Advance

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

jsx
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;
result
ButterMintLilac

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

jsx
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;
result
Selected: tag tag-lg tag-active / tag tag-lg tag-active
Default: tag tag-md / tag tag-md

Both 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 .scss files if you install sass; combine it with modules as Button.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 class instead of className.
  • 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.css in the file name, so styles is undefined.
  • Using styles.my-class (a hyphen is a minus); write styles['my-class'] or use camelCase.
  • Removing the focus outline with outline: none and no replacement.
  • Mixing three styling systems in one small project.

Exercise

  1. Turn the card example into a Card component with a CSS Module (in a project) and compare the class names in the browser's inspector.
  2. Add a variant prop to ProgressBar ("success", "warning", "danger") that picks a colour through a class instead of the inline color prop.
  3. Create light and dark themes with CSS variables on :root and a data-theme attribute toggled by a button.
  4. Install clsx in 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.

Check yourself

1. How do you add a CSS class to an element in JSX?
2. What problem do CSS Modules solve?
3. When is the style prop a good choice?
4. How do you import a CSS module in a component?