ReactStyled Components & CSS-in-JS

Styled Components & CSS-in-JS

CSS-in-JS is the approach of writing your CSS directly inside JavaScript files. The most influential library in this space is Styled Components, which introduced the tagged template literal syntax that the entire ecosystem later adopted. Understanding CSS-in-JS is important even if you choose not to use it — it shaped how the React community thinks about component-scoped styles and dynamic theming.

Installation

Bash
npm install styled-components
npm install --save-dev @types/styled-components
The Core Syntax

styled.tagName is a tagged template literal function. You write CSS inside the backticks exactly as you would in a stylesheet. Styled Components returns a real React component that renders the HTML element with those styles applied via a generated class name.

TSX
import styled from 'styled-components'

// Creates a <button> element with these styles
const Button = styled.button`
  display: inline-flex;
  align-items: center;
  padding: 8px 18px;
  font-size: 0.875rem;
  font-weight: 600;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  background: #0070f3;
  color: white;
  transition: background 0.15s ease;

  &:hover {
    background: #0051a2;
  }

  &:active {
    transform: scale(0.97);
  }
`

// Usage — it's just a React component
function App() {
  return <Button onClick={() => console.log('clicked')}>Save</Button>
}
Note
The `&` in styled-components refers to the component's own generated class name, just like Sass's parent selector. You can use it for pseudo-selectors, pseudo-elements, and nested elements.
Props-Based Dynamic Styling

This is where CSS-in-JS genuinely shines over CSS Modules. You can interpolate functions inside the template literal that receive the component's props, making dynamic styles feel completely natural:

TSX
import styled from 'styled-components'

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  fullWidth?: boolean
}

const Button = styled.button<ButtonProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  width: ${(props) => (props.fullWidth ? '100%' : 'auto')};

  /* Size variants */
  padding: ${({ size }) => {
    if (size === 'sm') return '4px 10px'
    if (size === 'lg') return '12px 24px'
    return '8px 18px'  /* md (default) */
  }};

  font-size: ${({ size }) => (size === 'sm' ? '0.75rem' : size === 'lg' ? '1rem' : '0.875rem')};

  /* Color variants */
  background: ${({ variant }) => {
    if (variant === 'secondary') return 'transparent'
    if (variant === 'danger') return '#dc2626'
    return '#0070f3'  /* primary (default) */
  }};

  color: ${({ variant }) => (variant === 'secondary' ? '#0070f3' : 'white')};

  border: ${({ variant }) => variant === 'secondary' ? '1.5px solid #0070f3' : 'none'};

  &:hover {
    opacity: 0.9;
  }
`

function App() {
  return (
    <div>
      <Button>Primary (default)</Button>
      <Button variant="secondary" size="sm">Secondary Small</Button>
      <Button variant="danger" size="lg" fullWidth>Delete Account</Button>
    </div>
  )
}
Theming with ThemeProvider

Styled Components ships a ThemeProvider that injects a theme object into every styled component via React context. This is the canonical pattern for design tokens in CSS-in-JS:

TSX
import { ThemeProvider, createGlobalStyle } from 'styled-components'

// Define your theme shape
const theme = {
  colors: {
    primary: '#0070f3',
    danger: '#dc2626',
    text: '#111827',
    background: '#ffffff',
    surface: '#f9fafb',
  },
  spacing: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
  radius: {
    sm: '4px',
    md: '6px',
    lg: '12px',
  },
}

type Theme = typeof theme

// Augment the DefaultTheme so TypeScript knows the shape
declare module 'styled-components' {
  export interface DefaultTheme extends Theme {}
}

// Now every styled component receives the theme via props.theme
const Card = styled.div`
  background: ${({ theme }) => theme.colors.surface};
  padding: ${({ theme }) => theme.spacing.md};
  border-radius: ${({ theme }) => theme.radius.lg};
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
`

const Title = styled.h2`
  color: ${({ theme }) => theme.colors.text};
  margin: 0 0 ${({ theme }) => theme.spacing.sm};
`

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Card>
        <Title>Hello, world</Title>
      </Card>
    </ThemeProvider>
  )
}
Global Styles with createGlobalStyle

TSX
import { createGlobalStyle } from 'styled-components'

const GlobalStyle = createGlobalStyle`
  *, *::before, *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: ${({ theme }) => theme.colors.background};
    color: ${({ theme }) => theme.colors.text};
  }
`

function App() {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <main>...</main>
    </ThemeProvider>
  )
}
Keyframe Animations

TSX
import styled, { keyframes } from 'styled-components'

const fadeIn = keyframes`
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
`

const spin = keyframes`
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
`

const Toast = styled.div`
  animation: ${fadeIn} 0.2s ease-out;
  background: #111;
  color: white;
  padding: 12px 16px;
  border-radius: 8px;
`

const Spinner = styled.div`
  width: 24px;
  height: 24px;
  border: 3px solid #e5e7eb;
  border-top-color: #0070f3;
  border-radius: 50%;
  animation: ${spin} 0.8s linear infinite;
`
The .attrs() Helper

.attrs() lets you attach default HTML attributes to a styled component, which avoids repeating them at every usage site:

TSX
// Without .attrs() you'd write type="button" everywhere
const Button = styled.button.attrs({ type: 'button' })`
  padding: 8px 16px;
`

// Useful for inputs with default attributes
const TextInput = styled.input.attrs({ type: 'text', autoComplete: 'off' })`
  border: 1px solid #d1d5db;
  border-radius: 4px;
  padding: 8px 12px;
`
SSR with Next.js

Styled Components injects styles at runtime. Without SSR setup, the server sends unstyled HTML and the styles flash in after hydration. In Next.js (App Router) install the official plugin:

Bash
npm install --save-dev babel-plugin-styled-components

JS
// next.config.js
const nextConfig = {
  compiler: {
    styledComponents: true,  // enables the SWC transform — no Babel needed
  },
}
module.exports = nextConfig
Emotion: The Alternative

Emotion is a direct Styled Components competitor with a nearly identical API. It is the styling engine behind MUI (Material UI) and Chakra UI. Emotion offers two entry points: @emotion/styled (same API as Styled Components) and @emotion/css (framework-agnostic). Emotion's compile-time extraction mode (@emotion/babel-plugin) is more mature than Styled Components', but the runtime cost remains.

Trade-off Summary

Concern

CSS-in-JS verdict

Co-location

Excellent — styles and logic in one file

Dynamic styling

First-class — interpolate any JS expression

Theming

Excellent — ThemeProvider + typed theme

Runtime cost

Real — style tag injection on every render

SSR

Needs extra setup — ServerStyleSheet or compiler plugin

Server Components (React 19)

Incompatible with runtime injection

Bundle size

Adds ~12–30 KB to client bundle

Industry trend

Declining — teams moving to Tailwind / CSS Modules

Warning
Runtime CSS-in-JS (Styled Components and Emotion's default mode) is fundamentally incompatible with React Server Components. If you use Next.js App Router with Server Components, do not use Styled Components at the top level. Use CSS Modules or Tailwind instead, or Emotion's compile-time extraction mode.
  • Use Styled Components only in existing codebases that already rely on it — refactoring away is a gradual process

  • Prefer CSS Modules or Tailwind for new projects — no runtime cost, SSR-safe, React 19-compatible

  • Use Emotion if you adopt MUI — it is baked into their rendering pipeline already

  • Typed themes via DefaultTheme augmentation are one of CSS-in-JS's best features — worth borrowing as a CSS custom property approach even in non-CSS-in-JS projects

Tip
If you are working in a Styled Components codebase and need to migrate incrementally, start by replacing the deepest leaf components (buttons, inputs, badges) with CSS Modules or Tailwind equivalents. The ThemeProvider at the top of the tree can be the last thing you remove.