Styling React Apps
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:
/* src/ProductCard.css */
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
}
.product-card .price {
font-weight: 700;
color: #087ea4;
}// 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:
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:
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:
/* src/Badge.module.css */
.badge {
padding: 2px 8px;
border-radius: 999px;
background: #e0f2fe;
}
.urgent {
background: #fee2e2;
}// 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:
npm install tailwindcss @tailwindcss/vite// 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:
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 isclassName. - Style strings.
style="color: red"fails; usestyle={{ color: "red" }}. - Dashes in style keys. Write
backgroundColor, notbackground-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
.titlewill affect each other. Use CSS Modules or more specific names.
Key points
- React uses normal CSS; connect it with
classNameor thestyleobject. - Inline style objects are best for dynamic values; they cannot do
:hoveror media queries. - Build conditional class names with an array and
join(" "), or theclsxpackage. - CSS Modules (
*.module.css) give each component private, generated class names. - Tailwind CSS 4 adds utility classes with the
@tailwindcss/viteplugin 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
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?
The style prop takes an object; a template literal builds the value with its unit.
2. What problem do CSS Modules solve?
Importing *.module.css returns an object of locally scoped class names.
3. Which of these can inline style objects NOT do?
Pseudo-classes, media queries and keyframes need real CSS, such as a stylesheet or CSS Module.
4. How is Tailwind CSS 4 added to a Vite React project?
Tailwind 4 uses its Vite plugin plus a single CSS import line.
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
