React course menu
How to Style React Components: CSS, Modules and Tailwind
Style React apps: importing CSS files with className, conditional classes, inline styles and when to use them, CSS Modules for scoped class names, Tailwind CSS utility classes, CSS variables for themes, and choosing an approach.
On this page
React does not dictate how you style components. These are the common approaches, from plain CSS to utility frameworks.
Plain CSS with className#
Write ordinary CSS and attach classes with className. In a Vite project you import the CSS file in a component (import "./Button.css";) and Vite adds it to the page. In the preview below the CSS block plays the role of that file:
.btn {
padding: 8px 16px;
border-radius: 6px;
border: 1px solid #4c6ef5;
background: white;
color: #364fc7;
cursor: pointer;
}
.btn-primary {
background: #4c6ef5;
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}export default function App() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<button className="btn">Cancel</button>
<button className="btn btn-primary">Save</button>
<button className="btn btn-primary" disabled>Saving…</button>
</div>
);
}Plain CSS is global: a .btn rule affects every element with that class anywhere in the app. Use naming conventions (like BEM: card__title) or CSS Modules to avoid clashes in large apps.
Conditional classes#
Build the class string with JavaScript:
.tab { padding: 6px 12px; border: 0; background: none; border-bottom: 2px solid transparent; cursor: pointer; }
.tab.active { border-bottom-color: #4c6ef5; color: #364fc7; font-weight: 600; }import { useState } from "react";
const TABS = ["Overview", "Reviews", "Shipping"];
export default function App() {
const [active, setActive] = useState("Overview");
return (
<div>
{TABS.map((tab) => (
<button key={tab} className={`tab ${tab === active ? "active" : ""}`} onClick={() => setActive(tab)}>
{tab}
</button>
))}
<p>Showing: {active}</p>
</div>
);
}For several conditions, the small clsx library is popular: className={clsx("tab", { active: isActive, disabled })}.
Inline styles#
The style prop takes an object. It is handy for values computed at runtime, such as a progress bar's width:
import { useState } from "react";
export default function App() {
const [done, setDone] = useState(3);
const total = 8;
const percent = Math.round((done / total) * 100);
return (
<div style={{ maxWidth: 300 }}>
<div style={{ height: 12, background: "#e9ecef", borderRadius: 6, overflow: "hidden" }}>
<div style={{ width: `${percent}%`, height: "100%", background: "#40c057", transition: "width .3s" }} />
</div>
<p>{done} of {total} lessons ({percent}%)</p>
<button onClick={() => setDone(Math.min(total, done + 1))}>Complete a lesson</button>
</div>
);
}Inline styles cannot express hover states, media queries or animations with keyframes, so use them for dynamic values and CSS for everything else.
CSS variables for themes#
Set custom properties from React and let CSS use them, which keeps styling in CSS while allowing dynamic themes:
.card { background: var(--bg); color: var(--fg); padding: 12px; border-radius: 8px; transition: background .2s; }import { useState } from "react";
export default function App() {
const [dark, setDark] = useState(false);
const theme = dark ? { "--bg": "#1f2430", "--fg": "#e9ecef" } : { "--bg": "#f1f3f5", "--fg": "#212529" };
return (
<div className="card" style={theme}>
<p style={{ marginTop: 0 }}>Themed with CSS variables.</p>
<button onClick={() => setDark(!dark)}>Toggle theme</button>
</div>
);
}CSS Modules#
A file named *.module.css is a CSS Module: its class names are made unique per file at build time, so they never clash. Vite and Next.js support them out of the box.
/* Button.module.css */
.button { padding: 8px 16px; border-radius: 6px; }
.primary { background: #4c6ef5; color: white; }// Button.jsx
import styles from "./Button.module.css";
export default function Button({ primary, children }) {
return <button className={`${styles.button} ${primary ? styles.primary : ""}`}>{children}</button>;
}styles.button becomes a generated name such as _button_1x2ab_1, unique to this file. (CSS Modules need a build step, so this example is shown as code for your own project.)
Tailwind CSS#
Tailwind provides small utility classes that you combine directly in className:
export default function Card({ title, children }) {
return (
<div className="rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md">
<h3 className="mb-2 text-lg font-semibold">{title}</h3>
<div className="text-gray-600">{children}</div>
</div>
);
}Utilities keep styles next to the markup and avoid naming classes; long class lists are the trade-off. Tailwind is set up with a plugin for Vite or Next.js (see the Tailwind documentation for current steps). This site itself is styled with Tailwind.
Which approach?#
Many projects combine two: for example Tailwind or CSS Modules for components plus inline styles for dynamic values.
Common mistakes#
Practice#
Practice exercise
Style a list of three notifications where unread ones have a bold title and a blue left border. Clicking a notification marks it read. Use a CSS block and conditional classes.
Show hint
CSS: .note.unread { border-left: 4px solid #4c6ef5; font-weight: bold; }. In JSX: className={"note" + (n.read ? "" : " unread")}.
Show solution
.note { padding: 8px 10px; border-left: 4px solid transparent; background: #f8f9fa; margin-bottom: 6px; cursor: pointer; }
.note.unread { border-left-color: #4c6ef5; font-weight: bold; }import { useState } from "react";
export default function App() {
const [notes, setNotes] = useState([
{ id: 1, text: "Your order has shipped", read: false },
{ id: 2, text: "New lesson: Custom Hooks", read: false },
{ id: 3, text: "Welcome to Laviyx Learn", read: true },
]);
const markRead = (id) => setNotes(notes.map((n) => (n.id === id ? { ...n, read: true } : n)));
return (
<div>
{notes.map((n) => (
<div key={n.id} className={"note" + (n.read ? "" : " unread")} onClick={() => markRead(n.id)}>
{n.text}
</div>
))}
</div>
);
}Key takeaway#
Finished this lesson?