Styling React Apps

React · Lesson 8 of 29 7 min read Updated

React does not have its own styling language. Everything you know from CSS still works; the question is only where you write it and how you connect it to your components. There are several popular options, and real projects often combine two of them.

In this lesson you will style components with ordinary CSS and className, use inline style objects for values that change, build class names conditionally, keep styles private with CSS Modules, and set up Tailwind CSS in a Vite project.

Option 1: a CSS file and className

The most familiar way is a normal CSS file. In a Vite project you import the CSS file from a component or from main.jsx, and Vite adds it to the page. You then use className (not class) on elements:

CSS
/* src/ProductCard.css */
.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 12px;
}
.product-card .price {
  font-weight: 700;
  color: #087ea4;
}
React JSX
// src/ProductCard.jsx
import "./ProductCard.css";

export default function ProductCard({ name, price }) {
  return (
    <article className="product-card">
      <h3>{name}</h3>
      <p className="price">₹{price}</p>
    </article>
  );
}

This is simple and fast. Its weakness is that all class names are global: a .price rule written for one component also affects every other element with that class. In big projects, teams use naming conventions or CSS Modules to avoid clashes.

Option 2: inline style objects

The style attribute takes a JavaScript object with camelCase property names. It is the natural choice when a value comes from data, such as the width of a progress bar:

React JSXTry it yourself
import { createRoot } from "react-dom/client";

function Progress({ label, done, total }) {
  const percent = Math.round((done / total) * 100);
  const colour = percent >= 75 ? "#15803d" : percent >= 40 ? "#b45309" : "#b91c1c";

  return (
    <div style={{ marginBottom: 12, fontFamily: "system-ui" }}>
      <div>{label}: {done} of {total} chapters ({percent}%)</div>
      <div style={{ background: "#e5e7eb", borderRadius: 6, height: 12, width: 260 }}>
        <div style={{ width: `${percent}%`, height: "100%", background: colour, borderRadius: 6 }} />
      </div>
    </div>
  );
}

createRoot(document.getElementById("root")).render(
  <>
    <Progress label="Physics" done={9} total={12} />
    <Progress label="Chemistry" done={5} total={11} />
    <Progress label="Maths" done={2} total={14} />
  </>
);

Numbers without a unit, such as borderRadius: 6, mean pixels for most properties. Inline styles have limits: no :hover, no media queries and no animations with @keyframes. Use them for a few dynamic values, and keep the rest in CSS.

A neat middle way is a CSS custom property: pass only the changing value inline, style={{ "--percent": `${percent}%` }}, and use width: var(--percent) in your stylesheet.

Conditional class names

Often you switch between a few looks: selected or not, error or valid. Build the className string from conditions:

React JSXTry it yourself
import { useState } from "react";
import { createRoot } from "react-dom/client";

const css = `
  .seat { width: 44px; height: 36px; margin: 4px; border: 1px solid #888; border-radius: 6px; background: white; cursor: pointer; }
  .seat:hover { border-color: #087ea4; }
  .seat.booked { background: #d1d5db; cursor: not-allowed; }
  .seat.selected { background: #087ea4; color: white; }
`;

const booked = ["A2", "B1"];

function SeatMap() {
  const [selected, setSelected] = useState("A3");
  const seats = ["A1", "A2", "A3", "B1", "B2", "B3"];

  return (
    <>
      <style>{css}</style>
      <p>Pick a seat for the Lucknow bus (selected: {selected})</p>
      {seats.map((seat) => {
        const isBooked = booked.includes(seat);
        const classes = ["seat", isBooked && "booked", seat === selected && "selected"]
          .filter(Boolean)
          .join(" ");
        return (
          <button key={seat} className={classes} disabled={isBooked} onClick={() => setSelected(seat)}>
            {seat}
          </button>
        );
      })}
    </>
  );
}

createRoot(document.getElementById("root")).render(<SeatMap />);

The array holds "seat" plus a class name or false for each condition; filter(Boolean) drops the false values and join(" ") makes the final string. Many projects use the tiny clsx package for the same job: clsx("seat", { booked: isBooked, selected: seat === selected }).

This example renders a <style> element from the component only because the Try-it editor cannot import CSS files. In a real project, put these rules in a .css file.

Option 3: CSS Modules

A CSS Module is a CSS file whose name ends in .module.css. Vite supports them without setup. When you import it, you get an object that maps your class names to unique generated names, so styles cannot leak into other components:

CSS
/* src/Badge.module.css */
.badge {
  padding: 2px 8px;
  border-radius: 999px;
  background: #e0f2fe;
}
.urgent {
  background: #fee2e2;
}
React JSX
// src/Badge.jsx
import styles from "./Badge.module.css";

export default function Badge({ text, urgent }) {
  return <span className={urgent ? `${styles.badge} ${styles.urgent}` : styles.badge}>{text}</span>;
}

In the browser the class becomes something like _badge_x7k2p_1, which no other file can accidentally reuse. CSS Modules are a good default when you like writing plain CSS.

Option 4: Tailwind CSS

Tailwind CSS gives you small utility classes such as p-4, rounded-lg and text-sky-700 that you combine directly in className. Version 4 sets up in a Vite project with one plugin:

Terminal
npm install tailwindcss @tailwindcss/vite
JavaScript
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Then replace the contents of src/index.css with the single line @import "tailwindcss";. Now components can use utilities:

React JSX
function Alert({ children }) {
  return <p className="rounded-lg border border-amber-300 bg-amber-50 p-3 text-amber-900">{children}</p>;
}

Tailwind keeps styles next to the markup and ships only the classes you use. The trade-off is long class strings; extracting repeated pieces into components keeps them manageable.

Common mistakes

  • Using class. In JSX it is className.
  • Style strings. style="color: red" fails; use style={{ color: "red" }}.
  • Dashes in style keys. Write backgroundColor, not background-color (unless it is a custom property such as "--gap").
  • Doing everything inline. Inline styles cannot handle hover, focus or media queries. Use classes for most styling.
  • Global name clashes. Two components both styling .title will affect each other. Use CSS Modules or more specific names.

Key points

  • React uses normal CSS; connect it with className or the style object.
  • Inline style objects are best for dynamic values; they cannot do :hover or media queries.
  • Build conditional class names with an array and join(" "), or the clsx package.
  • CSS Modules (*.module.css) give each component private, generated class names.
  • Tailwind CSS 4 adds utility classes with the @tailwindcss/vite plugin and @import "tailwindcss";.

Exercise

Write a MarksBar component that takes subject and marks (0 to 100). Show the subject and a bar whose width is the marks percentage. Use a CSS class low (red) for marks below 40 and high (green) for 80 and above, defined in a <style> element, and an inline style only for the width.

Show solution
React JSX
import { createRoot } from "react-dom/client";

const css = `
  .bar { height: 14px; background: #087ea4; border-radius: 4px; }
  .bar.low { background: #b91c1c; }
  .bar.high { background: #15803d; }
`;

function MarksBar({ subject, marks }) {
  const classes = ["bar", marks < 40 && "low", marks >= 80 && "high"].filter(Boolean).join(" ");
  return (
    <div style={{ width: 250, marginBottom: 8 }}>
      {subject}: {marks}
      <div className={classes} style={{ width: `${marks}%` }} />
    </div>
  );
}

createRoot(document.getElementById("root")).render(
  <>
    <style>{css}</style>
    <MarksBar subject="English" marks={86} />
    <MarksBar subject="Hindi" marks={62} />
    <MarksBar subject="Sanskrit" marks={35} />
  </>
);

The colour comes from classes, so it stays in CSS, while the width, which depends on data, is the only inline style.

Quick Quiz

1. Which is the correct way to set a dynamic width in JSX?

2. What problem do CSS Modules solve?

3. Which of these can inline style objects NOT do?

4. How is Tailwind CSS 4 added to a Vite React project?

Learn React with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call