Skip to content
Home
React Styling Guide: CSS Modules, Tailwind, and CSS-in-JS

React Styling Guide: CSS Modules, Tailwind, and CSS-in-JS

React React 8 min read 1649 words Beginner ExcellentWiki Editorial Team

Styling in React is not a solved problem — the ecosystem offers multiple valid approaches, each with distinct trade-offs. Unlike traditional multi-page websites where global CSS files are natural, React’s component model encourages co-located, scoped styles that travel with components.

Plain CSS and CSS Modules

The simplest approach is using regular CSS files imported into components. Create React App and Vite both support this out of the box.

CSS Modules

CSS Modules provide local scoping by default. When you import a .module.css file, the class names are hashed to prevent collisions:

/* Button.module.css */
.button {
  padding: 8px 16px;
  background: blue;
  color: white;
---
import styles from './Button.module.css';
export function Button({ children }) {
  return <button className={styles.button}>{children}</button>;
---

CSS Modules generate unique class names like Button_button_a3f2k, ensuring that .button in one file does not conflict with .button in another. They require no configuration in Vite, Next.js, or CRA.

Global CSS

For global styles (reset, typography, CSS custom properties), regular CSS imports work. In Next.js, import global CSS in the root layout; in Vite, import it in main.jsx.

Tailwind CSS

Tailwind CSS is a utility-first framework that has become the most popular styling approach in the React ecosystem. Instead of writing custom CSS, you compose utility classes directly in JSX:

function Card({ title, children }) {
  return (
    <div className="rounded-lg border border-gray-200 p-6 shadow-sm">
      <h2 className="text-xl font-semibold text-gray-900">{title}</h2>
      <p className="mt-2 text-gray-600">{children}</p>
    </div>
  );
---

Benefits

  • No context switching — styles live in the same file as markup
  • No naming conventions — no BEM, no CSS Modules naming debates
  • Built-in design system — consistent spacing, typography, and color scales
  • JIT compilation — Tailwind v3+ generates only the CSS you use, resulting in tiny production bundles

Configuration

Tailwind’s tailwind.config.js lets you extend the default design system:

module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: { brand: { 500: '#3b82f6' } },
    },
  },
---;

Performance

Tailwind’s JIT engine generates CSS on-demand during development, and purges unused styles in production. A typical production CSS file with Tailwind is 10-20 KB gzipped, comparable to hand-written CSS.

Styled Components and CSS-in-JS

CSS-in-JS libraries write CSS in JavaScript, typically using tagged template literals:

import styled from 'styled-components';

const Button = styled.button`
  padding: 8px 16px;
  background: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
  border-radius: 4px;
`;

Styled Components

Styled Components was the first widely adopted CSS-in-JS library. It generates unique class names automatically, supports props-based styling, and allows theming with a <ThemeProvider>.

Emotion

Emotion is a performant CSS-in-JS library that offers both the styled API and the css prop:

import { css } from '@emotion/react';

function Button({ primary }) {
  return (
    <button css={css`
      padding: 8px 16px;
      background: ${primary ? 'blue' : 'gray'};
    `}>
      Click me
    </button>
  );
---

The Runtime Debate

CSS-in-JS adds runtime overhead — styles are parsed and injected at runtime. For server-side rendering, this requires extra configuration. Since 2023, there has been a significant shift away from runtime CSS-in-JS toward zero-runtime alternatives:

  • Linaria: Compiles CSS-in-JS to static CSS files at build time
  • Vanilla Extract: Type-safe, zero-runtime CSS-in-JS with a static API

The React team’s position, as stated in discussions about React 19’s styling support, is that zero-runtime solutions align better with React’s future direction, especially with Server Components (which cannot use runtime CSS-in-JS).

CSS Custom Properties for Theming

CSS Custom Properties (CSS variables) enable dynamic theming without JavaScript overhead. They are inherited through the DOM tree and can be updated at runtime:

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
---

[data-theme="dark"] {
  --color-bg: #1a1a1a;
  --color-text: #ffffff;
---

body {
  background: var(--color-bg);
  color: var(--color-text);
---
function ThemeToggle() {
  const [theme, setTheme] = useState('light');
  useEffect(() => {
    document.documentElement.dataset.theme = theme;
  }, [theme]);
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>Toggle Theme</button>;
---

CSS variables are the most performant theming mechanism because they do not require JavaScript parsing or CSS-in-JS runtime. All major browsers support them, and they work with CSS Modules, Tailwind, and plain CSS.

Comparison and Decision Guide

ApproachScoped StylesRuntime CostSSR SupportEcosystem
Plain CSSManual (BEM)NoneNativeUniversal
CSS ModulesAutomaticNoneNativeUniversal
Tailwind CSSDesign systemNoneNativegrowing fast
styled-componentsAutomaticHighRequires configMature
EmotionAutomaticMediumRequires configMature
LinariaAutomaticNoneNativeGrowing

Theming and Design Tokens

Regardless of the styling approach, design tokens provide a consistent visual language across components. Design tokens are named values for colors, spacing, typography, and shadows:

:root {
  --color-primary: #3b82f6;
  --spacing-md: 16px;
  --radius-sm: 4px;
---

In Tailwind, design tokens are configured in tailwind.config.js:

theme: {
  extend: {
    colors: { primary: { 500: '#3b82f6' } },
    spacing: { 18: '4.5rem' },
  },
---

In CSS-in-JS, design tokens are JavaScript objects shared across styled components or Emotion’s ThemeProvider. A tokens-first approach ensures visual consistency regardless of the styling library chosen.

Responsive Design in React

Responsive design in React follows the same CSS principles as traditional web development but benefits from React’s component isolation:

Tailwind Responsive Prefixes

Tailwind provides breakpoint prefixes: sm:, md:, lg:, xl:, 2xl::

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">

Conditional Rendering with Custom Hooks

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
  useEffect(() => {
    const mql = window.matchMedia(query);
    setMatches(mql.matches);
    const handler = (e) => setMatches(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);
  return matches;
---

function ResponsiveComponent() {
  const isDesktop = useMediaQuery('(min-width: 1024px)');
  return isDesktop ? <DesktopLayout /> : <MobileLayout />;
---

This pattern avoids rendering hidden DOM elements, saving memory on mobile devices.

CSS Container Queries

Container queries allow components to respond to their container’s size rather than the viewport. This is particularly useful for reusable React components that appear in different contexts:

.card-container { container-type: inline-size; }
@container (min-width: 400px) {
  .card { display: grid; grid-template-columns: 200px 1fr; }
---

Container queries are supported in all modern browsers since 2024 and provide a more modular approach to responsive design compared to viewport-based breakpoints. Tailwind provides a @tailwindcss/container-queries plugin for utility-based container query styling.

Accessibility and Styling

Styling and accessibility are closely related. Ensure that styled components remain accessible:

  • Sufficient color contrast — WCAG AA requires 4.5:1 for normal text
  • Focus indicators — Never remove outline without providing an alternative
  • Reduced motion — Respect prefers-reduced-motion for users with vestibular disorders
  • Text resizing — Use relative units (rem, em) instead of fixed px for text
  • Screen readers — Use semantic HTML elements (<button>, <nav>, <main>) and ensure form inputs have associated <label> elements or aria-label attributes

Tailwind’s focus-visible:ring utilities and Radix UI’s focus management primitives provide accessible interactive elements out of the box.

Tailwind vs CSS-in-JS: The Current Landscape

Tailwind has become the default recommendation for new React projects. The utility-first approach reduces the amount of code you write and enforces design consistency. CSS-in-JS remains popular for component libraries and projects where dynamic styling is central.

The SPA framework Benchmark surveys consistently show Tailwind as the most popular styling approach among React developers, with satisfaction rates above 90%.

React Performance Best Practices

Performance optimization in React requires understanding when and why components re-render. React re-renders a component when its state changes, its parent re-renders, or the context it consumes changes. Unnecessary re-renders are the most common performance issue. Use React.memo to prevent re-renders when props have not changed — it performs a shallow comparison of props and skips rendering if they are identical. The useMemo hook memoizes expensive computation results, recalculating only when dependencies change. The useCallback hook memoizes function references, preventing child components from re-rendering due to new function references on every parent render. For lists, ensure each item has a stable, unique key prop — using array indices as keys causes bugs when items are reordered. Virtualization libraries like react-window and react-virtuoso render only visible items in long lists, dramatically reducing DOM nodes. Code splitting with React.lazy and Suspense loads components on demand, reducing initial bundle size. Profile your application with React DevTools Profiler before optimizing — measuring identifies actual bottlenecks that are worth fixing.

Testing React Components Effectively

React Testing Library encourages testing from the user’s perspective. Write tests that verify behavior, not implementation details. Use getByRole, getByLabelText, and getByText queries to find elements the way users do — by accessibility attributes and visible text. Avoid testing internal state or component internals; instead, test what the user sees and does. For async operations, use waitFor and findBy queries that retry until the element appears. Mock external dependencies at the network level using MSW (Mock Service Worker) rather than mocking imported modules. This creates more realistic tests that verify your integration with actual API contracts.

FAQ

Should I use Tailwind or CSS Modules?

Use Tailwind if you want a consistent design system with minimal custom CSS. Use CSS Modules if you prefer writing traditional CSS with scoping. Both are zero-runtime and work well together.

Does CSS-in-JS affect performance?

Runtime CSS-in-JS (styled-components, Emotion) adds ~8-12 KB gzipped and runtime parsing overhead. For most applications, the impact is negligible. For high-performance or server-rendered applications, consider zero-runtime alternatives like Linaria.

Can I use Tailwind with CSS-in-JS?

Yes. Some developers use Tailwind with Emotion or styled-components, though the Tailwind team recommends using Tailwind directly for simplicity.

How do I handle themes in React?

For Tailwind, use the dark: variant and CSS custom properties. For CSS-in-JS, use <ThemeProvider>. For CSS Modules, use CSS custom properties scoped to :root with JavaScript toggling.

Conclusion

Choose your styling approach based on your team’s preferences and project requirements. Tailwind CSS is the safest default — it has the largest community, best performance characteristics, and works seamlessly with React Server Components. CSS Modules are a close second for teams that prefer writing traditional CSS. The styled-components and Emotion documentation provide excellent guides for dynamic styling when needed. For integrating styling with component architecture, see React Components, and for build tool configuration, see React Deployment.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Authentication.

Section: React 1649 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top