Skip to content
React rc styling 5 min read

CSS-in-JS

CSS-in-JS moves your styles into JavaScript, colocated with the component that uses them. Instead of a separate .css file and string class names, you declare styles where the markup lives, get automatic scoping, and can drive any value from props or a theme. The two dominant libraries are styled-components and Emotion; both are mature, share most concepts, and produce real CSS at runtime. This page covers their syntax, props-based dynamic styling, theming, and the runtime-cost trade-off that has pushed many teams toward zero-runtime alternatives.

The styled component pattern

The signature feature is the styled component: a small wrapper component that carries its own styles. You call styled.<tag> with a tagged template literal of CSS, and you get back a real React component you render like any other.

// Button.jsx
import styled from "styled-components";

const Button = styled.button`
  background: #2563eb;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  cursor: pointer;

  &:hover {
    background: #1d4ed8;
  }
`;

export default function Toolbar() {
  return <Button onClick={() => alert("Saved")}>Save</Button>;
}

The CSS is plain CSS — nesting with &, pseudo-classes, media queries, and keyframes all work. At render time the library generates a unique class name (something like sc-bdfBwQ), injects the rule into a <style> tag in the document head, and applies that class to the element. Nothing leaks, because the class name is generated, not hand-written.

Emotion offers the same API under a different import, so most code is portable between the two:

import styled from "@emotion/styled";

const Card = styled.div`
  padding: 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
`;

Props-based dynamic styling

Because styles live in JavaScript, any interpolation in the template can be a function of the component’s props. This is where CSS-in-JS pulls ahead of static stylesheets: the styling logic and the data driving it sit together.

import styled from "styled-components";

const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  background: ${(props) => (props.$primary ? "#2563eb" : "#e5e7eb")};
  color: ${(props) => (props.$primary ? "white" : "#111827")};
  opacity: ${(props) => (props.disabled ? 0.5 : 1)};
`;

export default function Actions() {
  return (
    <>
      <Button $primary>Confirm</Button>
      <Button>Cancel</Button>
      <Button disabled>Pending</Button>
    </>
  );
}

Tip: In styled-components, prefix custom styling props with $ (a transient prop like $primary). The $ keeps the prop out of the rendered DOM, so you avoid React’s “unknown attribute” warnings for non-standard HTML attributes.

You can also build on existing styled components with styled(Component), which inherits the base styles and overrides only what you change — handy for variant systems.

Theming

Both libraries ship a ThemeProvider that puts a theme object into React context. Any styled component below it can read props.theme, so colors, spacing, and fonts come from one source of truth and can be swapped at runtime (for example, light/dark mode).

import styled, { ThemeProvider } from "styled-components";
import { useState } from "react";

const light = { bg: "#ffffff", fg: "#111827", accent: "#2563eb" };
const dark = { bg: "#0f172a", fg: "#f1f5f9", accent: "#60a5fa" };

const Panel = styled.div`
  background: ${(props) => props.theme.bg};
  color: ${(props) => props.theme.fg};
  border-bottom: 3px solid ${(props) => props.theme.accent};
  padding: 1.5rem;
`;

export default function App() {
  const [dark_mode, setDarkMode] = useState(false);
  return (
    <ThemeProvider theme={dark_mode ? dark : light}>
      <Panel>
        <button onClick={() => setDarkMode((d) => !d)}>Toggle theme</button>
      </Panel>
    </ThemeProvider>
  );
}

Toggling state re-renders the provider with a new theme object, and every themed component restyles automatically — no manual class swapping required.

The runtime-cost trade-off

The convenience has a price. Classic styled-components and Emotion are runtime libraries: they parse template literals, compute styles, and inject <style> tags while your app runs in the browser. That adds JavaScript to your bundle and CPU work on every render of a dynamic styled component, which becomes measurable in large lists or styling-heavy UIs. It also complicates server-side rendering (you must collect and flush the styles) and can cause a flash of unstyled content if mishandled.

Zero-runtime CSS-in-JS keeps the authoring experience but extracts the CSS at build time into a static .css file, so the browser ships no styling engine at all.

LibraryRuntime costDynamic via propsSSR setupNotes
styled-componentsYes (runtime)FullManual collectionLargest ecosystem
EmotionYes (runtime)FullManual collectionSmaller, flexible API
Vanilla ExtractNone (build time)Via variants/varsBuilt-inTyped styles in .css.ts
LinariaNone (build time)Via CSS variablesBuilt-instyled-like syntax

Warning: With build-time tools, prop-driven styling is expressed through predefined variants and CSS custom properties rather than arbitrary JS expressions. You trade some dynamic flexibility for zero runtime overhead — usually a good deal for production apps.

When to reach for it

CSS-in-JS shines for design systems and component libraries where colocation, theming, and prop-aware variants are core needs. For a typical product UI where performance is a priority, a zero-runtime option or CSS Modules often gives most of the benefit without the runtime tax. Match the tool to the constraint rather than the trend.

Best Practices

  • Define a single theme object and consume it via ThemeProvider instead of hardcoding colors in components.
  • Use transient props ($primary) so styling props never reach the DOM as invalid HTML attributes.
  • Define styled components at module scope, never inside a component body — recreating them each render breaks caching and remounts the DOM.
  • Prefer styled(Base) extension and shared variant maps over copy-pasting style blocks.
  • Reach for a zero-runtime library (Vanilla Extract, Linaria) when bundle size or render performance is critical.
  • Set up SSR style collection if you server-render, to avoid a flash of unstyled content.
Last updated June 14, 2026
Was this helpful?