mrkeyoor.com_
Thu 06 Aug 15:40 UTC
npmWeb Frontendupdated 06 Aug 2026

styled-components

styled-components lets you write real CSS inside tagged template literals and get back a React component with a generated, collision-proof class name. styled.button`...` produces a button; styled(Link)`...` wraps any component that accepts a className. Interpolated functions receive the component's props and the current theme, so a style can change based on a prop without you touching class name strings. It ships createGlobalStyle for resets, keyframes for scoped animations, ThemeProvider for design tokens over React context, ServerStyleSheet for server rendering, and a React Native build with the same API. Version 6 rewrote the internals in TypeScript so types come in the box, and it no longer guesses which props are style-only: you prefix those with a dollar sign to keep them off the DOM.

Verdict

Still the most pleasant way to write styles when they actually depend on runtime props, and v6 fixed the TypeScript story. On a React Server Components stack, or anywhere the render-time cost shows up in a profile, pick a zero-runtime option instead.

API stability4/5The styled.tag template API has been unchanged since 2017 and v6 kept it; the breaks were around the edges (transient props replacing automatic filtering, opt-in vendor prefixes, dropped Node and React floors), and v7 is still prerelease so nothing new has landed on stable users unannounced
Docs4/5styled-components.com covers the whole API with runnable examples and a v5-to-v6 migration page, and the README alone gets you productive; the weak spot is server rendering, where the App Router guidance is thin and most working setups come from Next.js docs or blog posts
Maintenance3/5Pushed within days and 6.5.0 released August 2026 with only about 12 open issues (19 counting PRs), which is unusually tidy; the README itself says the project is largely maintained by one person and asks for funding, and v7 has sat in prerelease
Ecosystem5/5Around 11M weekly downloads, 41k stars, first-class support in Next.js, Storybook, Jest via jest-styled-components, and stylelint; a large share of existing React design systems are built on it

Use it if

  • You want component-scoped CSS with no build step, no bundler plugin, and no separate .css files to keep in sync with your components
  • Your styles genuinely depend on runtime values (a chart bar width from data, a theme the user picks at runtime, an animation driven by state) rather than a fixed set of variants
  • You share a component library across React DOM and React Native and want one styling API for both
  • You are already on styled-components v5 and want the TypeScript-native rewrite, faster stylis 4 compiler, and the new createTheme CSS-variable bridge without changing your mental model
  • You need to style third-party components (react-router Link, a design system button) and would rather wrap them than fight their class names
Skip it if

Setup reality

npm install styled-components and it works in a plain Vite or CRA React app with no config and no @types package, because v6 ships its own TypeScript definitions. The peer range is React 16.8 or newer, react-dom and react-native are both optional peers, and Node 16 is the floor. Two things cost real time. First, server rendering: with the Next.js Pages Router you need babel-plugin-styled-components or the compiler flag plus a _document that flushes ServerStyleSheet, and with the App Router you have to write a client registry component that calls useServerInsertedHTML, mark it 'use client', and wrap your layout in it, or users see unstyled HTML on first paint. Second, theme typing: props.theme is an empty DefaultTheme until you add a styled.d.ts that declares module 'styled-components' and extends DefaultTheme with your token shape, and until you do that every theme access is a type error or an any. Vendor prefixes are off by default in v6, so if you support older Safari you add enableVendorPrefixes on a StyleSheetManager at the root.

Patterns

Style an element and vary it by propstyled-element-with-props

import styled from 'styled-components';

const Button = styled.button<{ $primary?: boolean }>`
  padding: 0.5rem 1.25rem;
  border-radius: 6px;
  border: 1px solid palevioletred;
  color: ${props => (props.$primary ? 'white' : 'palevioletred')};
  background: ${props => (props.$primary ? 'palevioletred' : 'transparent')};
`;

<Button>Cancel</Button>
<Button $primary>Save</Button>

The dollar-sign prefix marks a transient prop. Without it, v6 forwards `primary` to the real <button> and React warns about a non-boolean attribute in the console on every render.

Extend a styled component and style a third-party oneextend-and-wrap

const DangerButton = styled(Button)`
  border-color: tomato;
  color: tomato;
`;

import { Link } from 'react-router-dom';

const NavLink = styled(Link)`
  color: inherit;
  text-decoration: none;
  &:hover { text-decoration: underline; }
`;

styled(X) only works if X spreads a className prop onto a DOM node. Wrapping a component that swallows className produces a class that is generated and never applied, which looks like the CSS silently failing.

Make props.theme typed instead of emptytype-the-theme

// styled.d.ts
import 'styled-components';

declare module 'styled-components' {
  export interface DefaultTheme {
    colors: { bg: string; fg: string; accent: string };
    space: (n: number) => string;
  }
}

// anywhere
const Card = styled.div`
  background: ${p => p.theme.colors.bg};
  padding: ${p => p.theme.space(2)};
`;

This file must be included by tsconfig (inside your src include glob) or the module augmentation never loads and theme stays an empty interface. v6 needs no @types/styled-components; installing it will conflict.

Provide a theme and read it outside a templatetheme-provider-and-hook

import styled, { ThemeProvider, useTheme } from 'styled-components';

const dark = { colors: { bg: '#111', fg: '#eee', accent: '#7aa2f7' }, space: (n: number) => `${n * 4}px` };

function Chart() {
  const theme = useTheme();
  return <svg><line stroke={theme.colors.accent} /></svg>;
}

<ThemeProvider theme={dark}>
  <Chart />
</ThemeProvider>

useTheme returns undefined outside a ThemeProvider, so a component used in both contexts needs a fallback. For class components use the withTheme higher-order component instead.

Bridge a theme to CSS custom properties with createThemecss-variable-theme

import styled, { createTheme, ThemeProvider } from 'styled-components';

const theme = createTheme({
  colors: { bg: '#ffffff', fg: '#111111' },
  space: { md: '1rem' },
});

const Card = styled.div`
  background: ${theme.colors.bg};  /* var(--sc-colors-bg, #ffffff) */
  color: ${theme.colors.fg};
  padding: ${theme.space.md};
`;

<ThemeProvider theme={theme.raw}>
  <theme.GlobalStyle />
  <Card>Token-driven</Card>
</ThemeProvider>

The returned object is the theme with var() strings at every leaf; pass theme.raw to ThemeProvider and mount theme.GlobalStyle once. Do not do JS arithmetic on a leaf, it is a string like 'var(--sc-space-md, 1rem)'; use calc() instead.

Share a reusable style fragmentshared-css-block

import styled, { css } from 'styled-components';

const truncate = css`
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
`;

const Title = styled.h3<{ $clamped?: boolean }>`
  font-size: 1.125rem;
  ${p => p.$clamped && truncate}
  max-width: 24ch;
`;

Use css`` rather than a plain template string whenever the fragment contains interpolations; a raw string turns functions into '[object Object]' in the output CSS.

Define scoped keyframeskeyframes-animation

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

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

const Spinner = styled.div`
  width: 24px;
  height: 24px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: ${spin} 0.8s linear infinite;
`;

Interpolate the keyframes object, never its name as a string. Inside createGlobalStyle a keyframes object needs the css helper around the rule, otherwise the animation name is not injected.

Bake in default attributes and derived propsdefault-attrs

const PasswordInput = styled.input.attrs({
  type: 'password',
  autoComplete: 'current-password',
})`
  border: 1px solid #ccc;
  padding: 0.5em;
`;

const Meter = styled.div.attrs<{ $value: number }>(p => ({
  role: 'progressbar',
  'aria-valuenow': p.$value,
  style: { width: `${p.$value}%` },
}))`
  height: 8px;
  background: palevioletred;
`;

Anything that changes on nearly every render belongs in the inline `style` object as shown, not in the template. A distinct interpolated value generates a new class name and a new injected rule each time, which grows the stylesheet without bound.

Inject a reset and app-wide rulesglobal-styles

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  *, *::before, *::after { box-sizing: border-box; }
  body {
    margin: 0;
    font-family: system-ui, sans-serif;
    background: ${p => p.theme.colors.bg};
  }
`;

// render once, near the root
<ThemeProvider theme={dark}>
  <GlobalStyle />
  <App />
</ThemeProvider>

Render exactly one instance. Mounting the same GlobalStyle in two places injects the rules twice, and unmounting one of them removes them for both.

Server-render styles in the Next.js App Routernext-app-router-registry

'use client';
import { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export function StyledRegistry({ children }: { children: React.ReactNode }) {
  const [sheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = sheet.getStyleElement();
    sheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') return <>{children}</>;
  return <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>;
}

Wrap children in app/layout.tsx with this, and set compiler.styledComponents in next.config so class names match between server and client. Skip it and the first paint is unstyled HTML.

Keep unknown props off the DOM without renaming everythingfilter-props-globally

import isPropValid from '@emotion/is-prop-valid';
import { StyleSheetManager } from 'styled-components';

<StyleSheetManager
  shouldForwardProp={(prop, elementToBeCreated) =>
    typeof elementToBeCreated === 'string' ? isPropValid(prop) : true
  }
>
  <App />
</StyleSheetManager>

This is the v5 compatibility escape hatch when a v6 migration would mean renaming hundreds of props. Component-level styled.div.withConfig({ shouldForwardProp }) overrides this, and the check runs on every prop of every styled render.

Render the same styles as a different elementpolymorphic-as

const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 6px;
`;

<Button as="a" href="/pricing">See pricing</Button>

// lock it in permanently instead of per call site
const LinkButton = styled(Button).attrs({ as: 'a' })``;

TypeScript infers the new element's props from `as`, so href type-checks here. Swapping a button for an anchor changes keyboard and screen reader behavior, so add role or type attributes where the semantics no longer match.

Alternatives

PackageRegistryPick it when
@emotion/stylednpmYou want nearly the same API with a smaller core and the css prop, and you do not need the React Native target
@vanilla-extract/cssnpmYou want TypeScript-authored styles compiled to static CSS at build time with zero runtime
tailwindcssnpmYour styles are a fixed design system rather than runtime-computed, and you want no JS shipped for styling at all