Skip to content
Laviyx Learn

Try

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.

Intermediate10 min readUpdated
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:

CSS
.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;
}
JSX
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>
  );
}
Result

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:

CSS
.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; }
JSX
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>
  );
}
Result

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:

JSX
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>
  );
}
Result

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:

CSS
.card { background: var(--bg); color: var(--fg); padding: 12px; border-radius: 8px; transition: background .2s; }
JSX
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>
  );
}
Result

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.

CSS
/* Button.module.css */
.button { padding: 8px 16px; border-radius: 6px; }
.primary { background: #4c6ef5; color: white; }
JSX
// 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:

JSX
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?#

ApproachGood for
Plain CSSsmall apps, learning, global base styles
CSS Modulescomponent-scoped CSS without extra libraries
Tailwindfast, consistent styling in larger apps and teams
Inline stylesvalues calculated at runtime
CSS-in-JS (styled-components, Emotion)styles written in JavaScript; less common in new projects using server components

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
CSS
.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; }
JSX
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>
  );
}
Result

Key takeaway#

Next: Performance: memo, useMemo and useCallback.

Finished this lesson?