Styling in React

1. 📖 Introduction

Styling is one of the most opinionated areas of React development — there's no single "official" way to do it. Instead, React offers a rich ecosystem of approaches, from plain CSS stylesheets to fully-fledged CSS-in-JS libraries and utility-first frameworks like Tailwind CSS. This tutorial covers the full spectrum: core techniques, popular libraries, theming, responsiveness, animations, and the trade-offs that help you pick the right tool for each project.

Information

There's no single "correct" styling approach in React — the right choice depends on your team, project size, and performance needs. This guide will help you make an informed decision.

2. 🎨 Styling React Components

Because React components are just JS functions that return markup, styling can be attached in several fundamentally different ways: via external stylesheets, scoped modules, inline objects, or by generating CSS directly from your component code.

Styling Approaches
Traditional CSS
CSS-in-JS
Utility-First
Component Libraries
Stylesheets
CSS Modules
Styled Components
Emotion
Tailwind CSS
Material UI, Chakra, Ant Design

3. 📄 CSS Stylesheets

The simplest approach: write a regular .css file and import it directly into your component. Styles apply globally by default, based on class name.

Button.jsx

import "./Button.css";

function Button({ children }) {
  return <button className="btn-primary">{children}</button>;
}

Button.css

.btn-primary {
  background-color: #2563eb;
  color: white;
  padding: 8px 16px;
  border-radius: 6px;
}

Caution

Because plain CSS is global, class names can easily collide across components in larger projects.

4. 🧩 CSS Modules

CSS Modules solve the naming-collision problem by automatically scoping class names to the component that imports them. Files are named with a .module.css suffix.

Card.jsx

import styles from "./Card.module.css";

function Card({ title }) {
  return <div className={styles.card}><h3 className={styles.title}>{title}</h3></div>;
}

Card.module.css

.card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 16px;
}

.title {
  font-weight: 600;
}

Best Practice

CSS Modules give you the simplicity of plain CSS with automatic scoping — a great middle ground for teams new to CSS-in-JS concepts.

5. đŸ–Œī¸ Inline Styles

React also accepts a style prop containing a plain JS object, with camelCased property names instead of hyphenated CSS ones.

InlineStyle.jsx

function Alert() {
  return (
    <div style={{ backgroundColor: "#fee2e2", color: "#991b1b", padding: "12px" }}>
      Something went wrong!
    </div>
  );
}

Warning

Inline styles cannot use pseudo-classes like :hover, media queries, or keyframe animations — they're best reserved for small, one-off, or dynamically calculated values.

6. 🔀 Dynamic Styles

Because inline style objects and class names are just JS values, they can be computed from props or state at render time.

ProgressBar.jsx

function ProgressBar({ percent }) {
  return (
    <div style={{ width: "100%", background: "#e5e7eb" }}>
      <div style={{ width: `${percent}%`, background: "#22c55e", height: "8px" }} />
    </div>
  );
}

7. 🔁 Conditional Styling

Class names can be toggled based on state or props using template literals, ternaries, or a helper utility like clsx.

ConditionalStyle.jsx

import clsx from "clsx";

function Tab({ isActive, children }) {
  return (
    <button className={clsx("tab", isActive && "tab-active")}>
      {children}
    </button>
  );
}

Tip

Libraries like clsx and classnames make conditional class logic far more readable than manual string concatenation.

8. đŸŽšī¸ CSS Variables

CSS custom properties (variables) let you define reusable values — colors, spacing, fonts — that can be read and even overridden dynamically from React via inline styles.

variables.css

:root {
  --color-primary: #2563eb;
  --spacing-md: 16px;
}

.btn {
  background: var(--color-primary);
  padding: var(--spacing-md);
}

ThemedBox.jsx

function ThemedBox({ accentColor }) {
  return <div style={{ "--accent": accentColor }} className="themed-box" />;
}

9. 💅 Sass (SCSS)

Sass extends CSS with variables, nesting, mixins, and functions, then compiles down to plain CSS. It pairs naturally with CSS Modules as .module.scss.

Button.module.scss

$primary: #2563eb;

.btn {
  background: $primary;
  &:hover {
    background: darken($primary, 10%);
  }
}

10. đŸŽ¯ Less

Less is a similar CSS preprocessor to Sass, historically popular with libraries like Ant Design, which exposes Less variables for theme customization.

theme.less

@primary-color: #1890ff;

.btn {
  background: @primary-color;
  border-radius: 4px;
}

11. 💄 Styled Components

styled-components is a popular CSS-in-JS library that lets you write actual CSS inside tagged template literals, generating uniquely-scoped React components.

StyledButton.jsx

import styled from "styled-components";

const Button = styled.button`
  background: ${(props) => (props.primary ? "#2563eb" : "#e5e7eb")};
  color: ${(props) => (props.primary ? "white" : "black")};
  padding: 8px 16px;
  border-radius: 6px;

  &:hover {
    opacity: 0.9;
  }
`;

function App() {
  return <Button primary>Click me</Button>;
}

Read more in the Styled Components documentation.

12. 🎭 Emotion

Emotion is another popular CSS-in-JS library, offering both a styled API similar to Styled Components and a lightweight css prop for one-off styling.

EmotionExample.jsx

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

function Badge() {
  return (
    <span
      css={css`
        background: #fef3c7;
        padding: 4px 8px;
        border-radius: 999px;
      `}
    >
      New
    </span>
  );
}

13. đŸŒŦī¸ Tailwind CSS

Tailwind CSS is a utility-first framework: instead of writing custom CSS classes, you compose pre-defined utility classes directly in your JSX.

TailwindButton.jsx

function Button({ children }) {
  return (
    <button className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
      {children}
    </button>
  );
}

Tip

Tailwind's utility classes are purged at build time, so unused styles don't bloat your final bundle — resulting in very small production CSS files.

14. 🧱 Utility-First CSS

The utility-first philosophy behind Tailwind favors small, single-purpose classes (flex, p-4, text-center) over custom, semantically-named classes. This trades some markup verbosity for faster iteration and fewer naming decisions.

AspectUtility-FirstSemantic CSS
Naming overheadMinimalHigh (BEM, etc.)
Markup verbosityHigherLower
ReusabilityVia componentsVia class names

15. đŸ§Ŧ CSS-in-JS

CSS-in-JS is an umbrella term for libraries — like Styled Components and Emotion — that let you author CSS directly within JavaScript, gaining access to props, theme context, and dynamic logic.

  • Pros: automatic scoping, dynamic styling via props, colocated styles and logic
  • Cons: runtime overhead (for some libraries), extra bundle size, potential FOUC without proper SSR setup

16. đŸ…ąī¸ Styling with Bootstrap

Bootstrap can be used in React either via its plain CSS classes or through react-bootstrap, which wraps components as proper React elements.

BootstrapButton.jsx

import Button from "react-bootstrap/Button";

function App() {
  return <Button variant="primary">Click me</Button>;
}

17. 🎨 Styling with Material UI

Material UI (MUI) implements Google's Material Design system as ready-made React components, with a powerful sx prop and theming system for customization.

MuiButton.jsx

import Button from "@mui/material/Button";

function App() {
  return <Button variant="contained" sx={{ borderRadius: 2 }}>Click me</Button>;
}

18. 🌀 Styling with Chakra UI

Chakra UI combines accessible, composable components with a utility-style prop API and a built-in theming/dark-mode system.

ChakraButton.jsx

import { Button } from "@chakra-ui/react";

function App() {
  return <Button colorScheme="blue" borderRadius="md">Click me</Button>;
}

19. 🐜 Styling with Ant Design

Ant Design is a comprehensive component library popular in enterprise and admin dashboards, with theming powered by Less variables or a newer ConfigProvider token system.

AntdButton.jsx

import { Button } from "antd";

function App() {
  return <Button type="primary">Click me</Button>;
}

20. 🎭 Theme Management

Most styling solutions support a theme object — centralized colors, spacing, and typography — shared across the app via Context or a library's own ThemeProvider.

ThemeProvider.jsx

import { ThemeProvider } from "styled-components";

const theme = {
  colors: { primary: "#2563eb", danger: "#dc2626" },
  spacing: { sm: "8px", md: "16px" },
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      <MyApp />
    </ThemeProvider>
  );
}

Best Practice

Centralizing design tokens in a theme object avoids scattering hard-coded colors and spacing values throughout your codebase.

21. 🌙 Dark Mode

Dark mode can be implemented via a data-theme attribute on the root element combined with CSS variables, or through a library's built-in color mode support.

darkmode.css

:root {
  --bg: white;
  --text: black;
}

[data-theme="dark"] {
  --bg: #111827;
  --text: #f9fafb;
}

body {
  background: var(--bg);
  color: var(--text);
}

ThemeToggle.jsx

function ThemeToggle() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
  }, [theme]);

  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Toggle theme
    </button>
  );
}

22. 📱 Responsive Design

Responsive layouts adapt to different screen sizes using CSS media queries, flexible units, or a framework's built-in breakpoint system (like Tailwind's sm:, md:, lg: prefixes).

ResponsiveTailwind.jsx

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <Card />
  <Card />
  <Card />
</div>

responsive.css

.grid {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

23. 🧩 Component-Based Styling

Regardless of the underlying tool, styles in React are best organized per component — colocated with the markup they affect, rather than in one giant global stylesheet.

components
Button
Button.jsx
Button.module.css

24. 🌍 Global Styles

Some styles genuinely belong at the application level — CSS resets, base typography, and root CSS variables. These are typically imported once, in the app's entry file.

index.jsx

import "./index.css"; // global reset + base styles
import App from "./App";

25. 🔒 Scoped Styles

Scoped styling — via CSS Modules, Styled Components, or Emotion — ensures a component's styles cannot accidentally leak out and affect unrelated parts of the page.

Reference

Scoping is essentially automatic namespacing: the tool generates a unique class name (e.g. Card_title__x8f2A) so collisions become virtually impossible.

26. đŸŽŦ Animations

CSS @keyframes can be defined in a stylesheet and triggered by toggling a class name in React.

animations.css

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.fade-in {
  animation: fadeIn 0.3s ease-in;
}

FadeInBox.jsx

function Toast({ message }) {
  return <div className="fade-in">{message}</div>;
}

Tip

For more complex, interruptible animations, consider a dedicated library like Framer Motion, which integrates tightly with React's component lifecycle.

27. 🔀 Transitions

CSS transition smoothly animates property changes — like opacity or transform — whenever a class or inline style updates.

Collapsible.jsx

function Collapsible({ isOpen, children }) {
  return (
    <div
      style={{
        maxHeight: isOpen ? "500px" : "0px",
        overflow: "hidden",
        transition: "max-height 0.3s ease",
      }}
    >
      {children}
    </div>
  );
}

28. 🔤 Custom Fonts

Custom fonts can be loaded via @font-face, a package like @fontsource, or a <link> tag pointing to a font service such as Google Fonts.

fonts.css

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter.woff2") format("woff2");
  font-weight: 400;
}

body {
  font-family: "Inter", sans-serif;
}

29. 🧷 Icons

Icons are commonly used as React components from libraries like lucide-react or react-icons, giving you full control over size, color, and stroke via props — unlike static image icons.

IconExample.jsx

import { Heart } from "lucide-react";

function LikeButton() {
  return <Heart size={20} color="#dc2626" />;
}

30. đŸ–ŧī¸ Images & Backgrounds

Images can be rendered with a standard <img> tag, or applied as CSS backgrounds when used decoratively.

Hero.jsx

function Hero() {
  return (
    <div
      style={{
        backgroundImage: "url(/images/hero-bg.jpg)",
        backgroundSize: "cover",
        backgroundPosition: "center",
        height: "400px",
      }}
    />
  );
}

Tip

Use the native <img> element (rather than a CSS background) whenever the image conveys meaningful content, since backgrounds aren't announced to screen readers.

31. 🏆 Styling Best Practices

  1. Colocate styles with the component they belong to
  2. Centralize design tokens (colors, spacing, typography) in a shared theme
  3. Prefer scoped styling (CSS Modules or CSS-in-JS) over global class names in larger apps
  4. Keep dynamic, computed styles separate from static, reusable ones
  5. Pick one primary styling approach per project to avoid a mix of conflicting patterns

32. ⚡ Performance Optimization

Styling choices can meaningfully affect runtime performance, especially at scale.

  • Runtime CSS-in-JS libraries re-compute styles on every render unless memoized — extract static styles outside the component body
  • Utility-first frameworks like Tailwind ship pre-purged, static CSS, avoiding runtime style generation entirely
  • Avoid inline style objects created fresh on every render for large lists — they defeat memoization
  • Lazy-load large component libraries' unused modules to reduce bundle size

33. 🔷 TypeScript with Styling

Typing theme objects and styled-component props catches typos — like a missing color key — at compile time.

theme.d.ts

interface Theme {
  colors: {
    primary: string;
    danger: string;
  };
}

declare module "styled-components" {
  export interface DefaultTheme extends Theme {}
}

TypedButton.tsx

const Button = styled.button<{ primary?: boolean }>`
  background: ${(props) => (props.primary ? props.theme.colors.primary : "gray")};
`;

34. âš ī¸ Common Mistakes

  • Mixing too many styling approaches (e.g. Tailwind and Styled Components) in the same project
  • Writing overly specific, deeply nested CSS selectors that become fragile and hard to override
  • Forgetting that CSS Modules require styles.className access, not a plain string
  • Creating new inline style objects inside frequently re-rendered lists, hurting performance
  • Hard-coding colors instead of referencing theme tokens, making dark mode or rebranding painful later

Danger

Using a class name string directly instead of the imported styles object (e.g. className="card" instead of className={styles.card}) silently fails with CSS Modules, since the actual generated class name is different.

35. đŸ’Ŧ Frequently Asked Questions

Which styling approach should I choose for a new project?

For small projects, plain CSS or CSS Modules are often enough. For design-system-heavy apps, a component library like Material UI or Chakra UI saves time. For teams that want speed and consistency without custom CSS, Tailwind CSS is extremely popular.

Is CSS-in-JS slower than plain CSS?

Some runtime CSS-in-JS libraries add a small overhead since styles are generated in the browser. Newer zero-runtime alternatives and utility-first frameworks like Tailwind avoid this by generating static CSS at build time.

Can I combine Tailwind with a component library?

Yes — many teams use Tailwind for layout and spacing alongside a component library like Chakra UI or Material UI for pre-built, accessible widgets, though some visual conflicts can arise and should be tested carefully.

36. 📌 Summary

>>Good styling is invisible — it should feel effortless to the user and maintainable to the developer.