Styling in React
React has no opinion about how you style components — it renders DOM, and any CSS strategy that works on the web works in React. That freedom is a double-edged sword: there are five mainstream approaches, each with different trade-offs around scoping, performance, theming, and developer ergonomics. This page surveys plain/global CSS, CSS Modules, CSS-in-JS, utility-first frameworks like Tailwind, and inline styles, then gives you a practical framework for choosing. The rest of this section drills into each option in depth.
Plain and global CSS
The simplest approach is to write ordinary stylesheets and import them. With a bundler like Vite, importing a .css file injects it into the page; the rules are global, so a class defined anywhere applies everywhere.
// App.jsx
import "./App.css";
export default function App() {
return <button className="btn-primary">Save</button>;
}
/* App.css */
.btn-primary {
background: #2563eb;
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
}
Global CSS is zero-config and familiar, but the lack of scoping means class names can collide across a large app. It works well for resets, design tokens, and base typography that genuinely should be global.
CSS Modules
CSS Modules keep the syntax of plain CSS but make class names locally scoped by default. Name a file *.module.css, import it as an object, and the bundler rewrites each class to a unique hashed name.
// Button.jsx
import styles from "./Button.module.css";
export default function Button({ children }) {
return <button className={styles.primary}>{children}</button>;
}
/* Button.module.css */
.primary {
background: #2563eb;
color: white;
padding: 0.5rem 1rem;
}
The generated class might compile to Button_primary__a1b2c, so it can never clash with a .primary defined elsewhere. You get real CSS (media queries, pseudo-selectors, no runtime cost) with automatic encapsulation — a strong default for component-scoped styles.
CSS-in-JS
CSS-in-JS libraries such as styled-components or Emotion let you declare styles inside JavaScript, colocated with the component and able to read props. Styles are scoped automatically and can be fully dynamic.
import styled from "styled-components";
const Button = styled.button`
background: ${(props) => (props.$primary ? "#2563eb" : "#e5e7eb")};
color: ${(props) => (props.$primary ? "white" : "#111")};
padding: 0.5rem 1rem;
border-radius: 6px;
`;
export default function Toolbar() {
return <Button $primary>Save</Button>;
}
The colocation and prop-driven styling are excellent for design systems, but classic runtime CSS-in-JS adds JavaScript work to render and can hurt performance at scale. Newer zero-runtime options (Vanilla Extract, Linaria) extract the CSS at build time to address this.
Utility-first (Tailwind)
Tailwind CSS provides small single-purpose classes you compose directly in markup. There is no separate stylesheet to maintain per component; you build the look in className.
export default function Button({ children }) {
return (
<button className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
{children}
</button>
);
}
Tailwind is fast to write, keeps styles colocated without runtime cost, and ships only the classes you actually use after its build step. The trade-off is verbose markup and a learning curve for the utility vocabulary.
Inline styles
React supports the style prop, which takes an object of camelCased CSS properties. It is great for a handful of dynamic, computed values.
export default function ProgressBar({ percent }) {
return (
<div style={{ background: "#e5e7eb", borderRadius: 4 }}>
<div style={{ width: `${percent}%`, height: 8, background: "#2563eb" }} />
</div>
);
}
Inline styles are scoped to the element and trivially dynamic, but they cannot express pseudo-classes (:hover), media queries, or keyframes, and they create a new object on every render. Use them for one-off computed values, not as a general styling system.
Gotcha: Inline style keys are camelCased JS properties (
backgroundColor, notbackground-color), and numeric values default to pixels (height: 8means8px). Use a string for other units.
Comparison
| Approach | Scoping | Dynamic styles | Runtime cost | Best for |
|---|---|---|---|---|
| Plain / global CSS | Global | Hard | None | Resets, tokens, base styles |
| CSS Modules | Local (auto) | Limited | None | Component-scoped CSS |
| CSS-in-JS | Local (auto) | Excellent | Some (or none if zero-runtime) | Design systems, theming |
| Utility-first (Tailwind) | N/A (atomic) | Via class toggling | None | Rapid UI, consistent spacing |
| Inline styles | Element only | Excellent | Per render | One-off computed values |
How to choose
Pick based on team and project, not fashion. For a small app or prototype, global CSS or Tailwind gets you moving fastest. For a component library or large codebase where collisions matter, CSS Modules or CSS-in-JS give you encapsulation. If you want prop-aware styling and theming as a first-class concern, CSS-in-JS shines; if you want zero runtime and consistent design tokens, Tailwind or CSS Modules are safer. Most real apps combine strategies: global CSS for resets and tokens, plus one scoped approach for components, plus inline styles for the occasional computed value.
Best Practices
- Choose one primary scoped approach per project and apply it consistently; reserve global CSS for resets and design tokens.
- Prefer build-time solutions (CSS Modules, Tailwind, zero-runtime CSS-in-JS) over runtime CSS-in-JS when performance matters.
- Use inline
styleonly for dynamic, computed values — never for static styling that belongs in a stylesheet. - Keep design tokens (colors, spacing, fonts) in one place so theming stays consistent across approaches.
- Avoid deeply nested selectors and
!important; rely on scoping instead of specificity wars. - Toggle classes conditionally rather than recomputing inline style objects, to keep renders cheap and styles cacheable.