styled-components v7

What's new in v7

Note

v7 is in alpha. This release is under active development, and the docs will receive frequent updates over the next few weeks as internals are refined.

Install the prerelease from npm’s @test dist-tag:

npm install styled-components@test

v7 is an architectural reform for the web and the start of a new chapter for styled-components/native. We replaced stylis with an in-house CSS parser, rewrote the native runtime from the ground up, and set a clear direction for native: the same CSS should feel the same everywhere, whether you render on web, iOS, or Android.

The React Native CanIUse page shows how far v7 has moved that goal already, and where there is still platform work to do.

React Native CanIUse compatibility matrix preview

The bottleneck now is funding. styled-components can help provide a universal React visual surface while strengthening React Native's styling ecosystem at the same time; recent upstream React Native PRs are already filling gaps around textAlignVertical, textDecorationStyle, textDecorationColor, and text decoration rendering. Donations and sponsorship make it possible to do that work outside of "passion time".

Highlights

  • In-house CSS parser: replaces stylis as the runtime engine. No more :is() / :where() / :has() recursion bugs, no more silent breakage on modern at-rules.
  • Modern CSS on React Native: @media, @container, @supports, container queries, viewport units (vw, dvh, cqw), font-relative units (rem, em, lh), oklch / oklab / lab / lch / color-mix / light-dark() / relative-color syntax, clamp / min / max / round / sin / cos and the full Math L4 family, logical shorthands (margin-inline, border-inline, …), gradients, and the full filter chain all work in styled.View\...``.
  • Selectors and combinators on React Native: attribute selectors with every operator (~=, ^=, $=, *=, |=, i flag), :not(<simple>) and :has(<simple>), tree-structural pseudos (:first-child, :nth-child(an+b), :nth-of-type), and the four combinators (Foo &, Foo > &, Foo + &, Foo ~ &) between styled-component references. Pseudo-state selectors (:hover, :focus, :pressed, :disabled) work via React Native's existing event surfaces. The CSS Compatibility matrix breaks down every feature by version and raw platform support.
  • Native animations: transition, @keyframes, and @starting-style all run on React Native out of the box via the default Animated-based adapter. An optional reanimated adapter is available for consumers who prefer it (experimental, probably doesn't work yet).
  • createTheme() works on native: the same theme contract from web. <ThemeProvider> deep-merges nested themes on native.
  • Plugins subpath: import { rtlPlugin, rscPlugin } from 'styled-components/plugins'. First-party RTL replaces stylis-plugin-rtl. Custom plugins move to a narrower SCPlugin interface (name, rw, decl).
  • extractCSS(): read the current stylesheet as plain text. Replaces the legacy disableCSSOMInjection toggle. You asked, we listened.
  • Global styles emit once: mounting the same createGlobalStyle component multiple times now emits its CSS only once.
  • Faster SSR at scale.
  • Remapping CSS into native props: function-form .attrs((props, ast) => ...) accepts a second argument that lifts declared style values or theme tokens onto the rendered component as props. Most useful for React Native libraries that style via props (e.g. react-native-svg's <Path fill="..." />, Image's tintColor, icon libraries). Details →

Removed from v7

  • defaultProps no longer flows through styled components (React 19 mostly removed it).
  • disableCSSOMInjection prop on StyleSheetManager and the SC_DISABLE_SPEEDY env vars have been removed; reach for extractCSS() instead.
  • enableVendorPrefixes has been removed. Modern browser targets handle prefixing themselves; for the few properties that still need them (e.g. -webkit-backdrop-filter on Safari), declare both forms in your CSS or run a build-time PostCSS transform.
  • css-to-react-native is no longer a peer dependency, the functionality has been folded into core.
  • The top-level stylisPluginRSC export moved to styled-components/plugins as rscPlugin. The stylisPlugins prop on <StyleSheetManager> is now plugins.

Peer dependency floors

  • react and react-dom ≥ 19.0.0
  • react-native ≥ 0.85.0 (optional peer)
  • react-native-reanimated ≥ 4.0.0 (optional peer, only required if you opt into the reanimated animation adapter)

Older React or React Native projects should stay on v6.

Migrating from v6

Most v6 apps move to v7 by bumping the version, updating peers, and applying a few small changes. The list below is exhaustive; pick out the items that apply.

Install the current v7 prerelease with npm install styled-components@test.

Update peers

  • react and react-dom to ^19.
  • react-native to ^0.85 if you use it.
  • react-native-reanimated to ^4 if you opt into the native animation adapter.
  • Drop css-to-react-native if it was in your package.json only for styled-components.

defaultProps is no longer honored

React 19 removed defaultProps from function components, so styled components can no longer inherit them either. Use .attrs() for prop defaults and <ThemeProvider> for default themes.

// Before (v6, no longer applies in v7)
const Button = styled.button``;
Button.defaultProps = { type: 'button' };


// After: object form always wins
const Button = styled.button.attrs({ type: 'button' })``;


// After: function form lets user props override
const Button = styled.button.attrs<{ type?: string }>(p => ({
  type: p.type ?? 'button',
}))``;

disableCSSOMInjection and SC_DISABLE_SPEEDY removed; use extractCSS()

The browser build always uses the fast injection path. If you were toggling into a slower text-based mode to read the CSS as a string (for static-render pipelines, micro-frontend cloning, embedding into iframes or Shadow DOM), call the new extractCSS():

import { extractCSS } from 'styled-components';


// after rendering
const css = extractCSS();

The result is plain CSS, safe to inject into another document via innerHTML. The v7 SSR escaping rule applies: </style> substrings in interpolated values are rendered as the CSS hex escape \3C/style so they cannot terminate a host <style> tag.

enableVendorPrefixes removed

The runtime vendor prefixer has been removed. For the properties that still need prefixing on older Safari (backdrop-filter, mask, user-select), declare both the prefixed and unprefixed forms in your CSS, or run a build-time PostCSS transform.

stylisPluginRSC moved to a subpath

The top-level export has been removed. Import from styled-components/plugins instead, and pass via the renamed plugins prop:

-import { stylisPluginRSC } from 'styled-components';
+import { rscPlugin } from 'styled-components/plugins';

-<StyleSheetManager stylisPlugins={[stylisPluginRSC]}>
+<StyleSheetManager plugins={[rscPlugin]}>

RTL is now first-party

stylis-plugin-rtl is no longer needed:

-import rtl from 'stylis-plugin-rtl';
+import { rtlPlugin } from 'styled-components/plugins';

-<StyleSheetManager stylisPlugins={[rtl]}>
+<StyleSheetManager plugins={[rtlPlugin]}>

rtlPlugin swaps physical side properties (padding-leftpadding-right), flips left / right keyword values on float / clear / text-align / caption-side, and mirrors 4-value shorthand positions. Logical properties like margin-inline-start pass through unchanged.

Custom stylis plugins need to port

Custom plugins authored against the v6 stylis contract no longer load. Implement the v7 plugin shape:

import type { SCPlugin } from 'styled-components/plugins';


const scopePlugin: SCPlugin = {
  name: 'scope',
  // `rw` runs on every fully-resolved selector after `&` substitution and
  // namespace prepending. Return a new selector string.
  rw: selector => `.app ${selector}`,
  // `decl` runs on every emitted `prop: value` pair. Return `{ prop, value }`
  // to rewrite, or `undefined` to leave the pair unchanged.
  decl: (prop, value) => ({ prop, value }),
};

Legacy stylis function plugins don't run in v7 because they don't expose rw or decl. Production builds ignore them; development builds warn once per unrecognized plugin name.

Global styles emit once per component

createGlobalStyle components now emit their CSS once even if you mount the same component multiple times. If your app deliberately remounts the same global to re-apply styles, that pattern no longer works; mount once per global rule set.

React Native bumps and changes

The CSS Compatibility matrix is the per-feature source of truth for what changed between v6 and v7 on web and native; the list below covers the most common cases.

If you're on React Native:

  • The peer floor moves to RN ≥ 0.85.
  • The lab(), lch(), oklab(), oklch(), and color-mix() notations now resolve to displayable colors with hue-preserving gamut mapping. Wide-gamut inputs that fall outside sRGB land at the closest in-gamut color.
  • transform: matrix(...) / matrix3d(...) and bare-number translateX(N) work on native.
  • linear-gradient(...), radial-gradient(...), and the full filter chain (blur, saturate, hue-rotate, ...) work on native. iOS apps need ReactNativeReleaseLevel: experimental in Info.plist to enable the SwiftUI filter backend for blur / saturate / hue-rotate / grayscale / contrast / drop-shadow. brightness and opacity work without it.
  • border: none now emits border-style: none (previously solid, which produced surprising hairlines).
  • transition, @keyframes, and @starting-style all animate on the native thread by default — no extra import, peer dependency, or configuration. If you prefer to drive animations through reanimated 4's CSS layer, opt in once at your app entry with import 'styled-components/native/reanimated'; react-native-reanimated@^4 becomes a peer dependency only in that case.

TypeScript

  • IInlineStyle and IInlineStyleConstructor were renamed to INativeStyle and INativeStyleConstructor. The old names are exported as deprecated aliases.
  • Stringifier is exported as a deprecated alias for the new Compiler interface (the shape differs, so consuming code that called the type as a function will need to migrate to Compiler.compile / Compiler.emit).
  • Flattener and NameGenerator are gone (private helpers, never documented).

React Native gets modern CSS

The same template literal syntax that works in styled.div works in styled.View in v7. Same createTheme(), same selectors, same animations. Differences come down to what React Native can render on the platform.

Modern CSS that works in styled.View\...``

import styled from 'styled-components/native';


const Card = styled.View`
  width: clamp(240px, 80vw, 480px);
  background-color: light-dark(white, #111);
  padding: 24px;
  border-radius: 8px;


  @container card (min-width: 320px) {
    padding: 32px;
  }
`;
  • Math functions: calc(), clamp(), min(), max(), plus the full CSS Values 4 Math L4 family — round(), mod(), rem(), sin, cos, tan, asin, acos, atan, atan2, pow, sqrt, hypot, log, exp, abs, sign. Constants pi and e resolve in any context.
  • Modern color spaces: oklch(...), oklab(...), lch(...), lab(...), color-mix(in <space>, ...). Wide-gamut inputs that fall outside sRGB are gamut-mapped to the closest in-gamut color while preserving hue. Percent channels follow CSS Color L4 ranges (lab(50% 0 0) is mid-gray).
  • Relative-color syntax (CSS Color 5) for the four modern spaces: oklch(from #ff0000 calc(l - 0.15) c h). Theme-token bases work too: oklch(from ${theme.colors.brand} calc(l - 0.15) c h).
  • color-mix(in <space>, …) through srgb, oklab, oklch, lab, or lch.
  • CSS Color 4 system color keywords auto-switch with OS appearance: canvas, canvastext, field, fieldtext, graytext, highlight, highlighttext, linktext, visitedtext, activetext.
  • Viewport units: vw, vh, vi, vb, vmin, vmax, plus the s* / l* / d* variants (dvh, svw, lvi).
  • Container query units: cqw, cqh, cqmin, cqmax.
  • Font-relative units: rem, em, lh, rlh resolve against the cascade at render.
  • light-dark(light, dark) swaps based on OS appearance.
  • Logical shorthands: margin-inline, margin-block, padding-inline, padding-block, inset-inline, inset-block, plus the full border-inline / border-block family.
  • line-clamp: N truncates <Text> to N lines. text-wrap: nowrap collapses to a single line; text-wrap-style: balance / pretty improve line-breaking on Android.
  • text-align: start | end | match-parent resolves correctly under RTL on both platforms.
  • aspect-ratio with bare ratio, slash form, auto, and the two-value auto <ratio> form.
  • transform: matrix(...), matrix3d(...), and bare numbers in translateX(10). Top-level perspective: 1000px works without folding it into a transform array.
  • font-style: oblique falls back to italic; font-family recognizes the 13 generic CSS keywords (system-ui, ui-*, sans-serif, serif, monospace, cursive, fantasy, emoji, math, fangsong).
  • place-items and place-self for the align axis.
  • field-sizing: content auto-grows a <TextInput> to its content.
  • interactivity: inert suppresses interaction and hides the subtree from screen readers.

env(safe-area-inset-*) caveat. The function parses correctly but currently resolves to 0 — the integration with react-native-safe-area-context is not wired yet. Use useSafeAreaInsets() directly until that lands.

Selectors on React Native

const Toggle = styled.Pressable<{ 'aria-pressed'?: boolean }>`
  background: white;


  &[aria-pressed='true'] {
    background: yellow;
  }


  &:pressed {
    transform: scale(0.95);
  }


  &:focus {
    outline-color: dodgerblue;
  }
`;

The same CSS works on web and native. Anchor pseudo-states with & (bare :hover parses as a descendant selector, not a state).

Pseudo-states. &:hover, &:focus, &:focus-visible, &:pressed, &:disabled. Hover on native uses Pressable's pointer events; React Native ≥ 0.85.

Attribute selectors. The full CSS Selectors 4 grammar works on native:

  • Presence: &[attr]
  • Exact: &[attr=value]
  • Word: &[class~=item]
  • Prefix: &[lang|=en]
  • Starts-with: &[href^="https"]
  • Ends-with: &[src$=".png"]
  • Substring: &[data-state*="hidden"]
  • Case-insensitive flag: &[data-tag="X" i]

Compound brackets AND-evaluate (&[a][b]); a trailing pseudo-state attaches (&[a]:active). Booleans coerce, so aria-pressed={true} and aria-pressed="true" both match [aria-pressed=true].

:not(<simple>) and :has(<simple>). Single-argument forms work on native:

const Row = styled.View`
  &:not([disabled]) { opacity: 1; }
  &:has(${Avatar}) { padding-inline-start: 56px; }
`;

:not() accepts a single pseudo-state or a single attribute selector. :has() accepts a styled-component reference (matches when that component appears as a descendant) or a single attribute selector. Complex inner arguments warn in dev and don't match.

:is() and :where(). Apply the rule to each listed state: &:is(:hover, :focus).

Tree-structural pseudos. :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type, and the functional :nth-*(<an+b>) family with odd / even keywords. Siblings need to be inside a styled-component parent for indexing to work — a non-styled wrapper in between resets the count.

Combinators between styled-component references.

const Card = styled.View``;
const Title = styled.Text`
  ${Card} & { color: dodgerblue; }     // any descendant of Card
  ${Card} > & { font-weight: bold; }    // direct child of Card
  ${Card} + & { margin-top: 8px; }      // immediately after a Card sibling
  ${Card} ~ & { opacity: 0.6; }         // anywhere after a Card sibling
`;

Descendant matching (${Foo} &) is transparent to non-styled wrappers. The child combinator (>) requires the matched element to be a direct child of a published parent — a non-styled wrapper in between will break the match.

Animations

CSS transition animates by default. No setup, no extra imports.

const Card = styled.View`
  background-color: ${p => p.$bg};
  transition: background-color 280ms ease-out;
`;


// Tapping a button that flips $bg from 'red' to 'blue' animates smoothly.

Eligible properties (opacity, every color, all border radius corners, transforms, shadows, filter) run on the native thread. @keyframes and @starting-style (first-mount enter animations) work the same way. animation-direction, animation-fill-mode, animation-play-state, animation-iteration-count, animation-composition (replace | add | accumulate), and per-frame easing all work without extra imports.

CSS easing matches the W3C spec curves: ease, ease-in, ease-out, ease-in-out, cubic-bezier(), steps(), linear(). Note: the CSS ease keyword is the W3C ease curve, not React Native's Easing.ease (which is ease-in). prefers-reduced-motion is honored — durations collapse to 0 when the OS setting is on.

If you'd rather drive animations through reanimated, the optional adapter is one import:

import 'styled-components/native/reanimated';

react-native-reanimated@^4 is an optional peer; install it yourself if you opt in.

Remapping CSS into native props

Many React Native libraries take styling through component props rather than the style prop — react-native-svg's <Path fill="..." stroke="..." />, charting libraries with tintColor, icon libraries with color. The function form of .attrs((props, ast) => ...) accepts a second ast argument that lets you read CSS declarations or theme tokens and rewrite them onto the rendered component as props:

import styled from 'styled-components/native';
import { Path } from 'react-native-svg';
import { Image } from 'react-native';


// Author Icon with CSS; render as <Path fill="red" stroke="navy" />
const Icon = styled(Path).attrs((_props, ast) => ({
  fill: ast.pop('color'),
  stroke: ast.pop('borderColor', 'black'),
}))`
  color: red;
  border-color: navy;
`;


// Lift a theme token onto Image's tintColor prop
const Logo = styled(Image).attrs((_props, ast) => ({
  tintColor: ast.pop('palette.brand.primary'),
}))`
  width: 32px;
  height: 32px;
`;

ast.pop(key) reads the value and prevents the declaration from reaching the rendered style. ast.peek(key) reads without removing — use it when both the prop and the CSS declaration should flow through. Both accept an optional fallback.

The first argument dispatches on shape: a CSS property name ('color', 'borderColor') reads a resolved declaration; a dot-separated path ('palette.brand.primary') reads from the active theme with autocomplete and type inference from your augmented DefaultTheme.

createTheme() on native

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


const theme = createTheme({
  colors: { bg: '#ffffff', text: '#111111' },
});


const Card = styled.View`
  background-color: ${theme.colors.bg};
  border-color: ${theme.colors.text};
`;


<ThemeProvider theme={{ colors: { bg: '#111', text: '#eee' } }}>
  <Card />
</ThemeProvider>

Web and native share the same theme contract. Nested <ThemeProvider> calls deep-merge on native so an inner override that only touches one leaf keeps the siblings it inherited.

Limitations on native

These spec features pass through to react-native-web but do not render on React Native today (the compatibility matrix is the per-feature source of truth):

  • ::before / ::after / ::placeholder / ::marker / ::selection
  • backdrop-filter, mask / mask-image (community packages cover both)
  • background-image: url() for raster images (gradient background-image works)
  • clip-path
  • position: fixed, position: sticky, position: anchor() / anchor-name
  • text-indent, word-spacing, text-box-trim, text-spacing-trim
  • transform-style: preserve-3d (basic rotateX / rotateY / translateZ / perspective do work)
  • CSS Grid (display: grid, grid-template-*)
  • Form-state pseudos (:invalid, :required, :read-only, :checked)
  • Scroll-snap, view transitions, scroll-driven animations

Plugins moved to a dedicated subpath

import { StyleSheetManager } from 'styled-components';
import { rtlPlugin, rscPlugin } from 'styled-components/plugins';


<StyleSheetManager plugins={[rtlPlugin, rscPlugin]}>
  <App />
</StyleSheetManager>

The stylisPlugins prop on <StyleSheetManager> is now plugins. The top-level stylisPluginRSC export moved into the new subpath as rscPlugin. Tree-shaking removes plugins from any app that doesn't import them.

rtlPlugin

Replaces stylis-plugin-rtl for users coming from v6:

  • Swaps physical side properties (padding-leftpadding-right, margin-leftmargin-right, border-leftborder-right, etc.).
  • Flips left / right keyword values on float, clear, text-align, and caption-side.
  • Mirrors 4-value shorthand positions (margin: 0 4px 0 8px; becomes margin: 0 8px 0 4px).
  • Logical properties like margin-inline-start pass through unchanged because they already resolve correctly per the document direction.

rscPlugin

Rewrites child-index selectors so they ignore inline <style> tags emitted by React Server Components. Without it, :first-child / :last-child / :nth-child() don't match the elements you'd expect inside a server component, because RSC injects an inline <style> ahead of each affected component.

import { StyleSheetManager } from 'styled-components';
import { rscPlugin } from 'styled-components/plugins';


<StyleSheetManager plugins={[rscPlugin]}>
  <App />
</StyleSheetManager>

Use this in apps that render via react-server-dom-* (Next.js App Router server components, Vite RSC plugins, etc).

Authoring custom plugins

The v7 plugin contract is narrower than the stylis-style middleware shape used in v6. A plugin is a plain object with a name, plus either or both of rw (selector rewrite) and decl (declaration rewrite):

import type { SCPlugin } from 'styled-components/plugins';


const scopePlugin: SCPlugin = {
  name: 'scope',
  // `rw` runs on every fully-resolved selector after `&` substitution and
  // namespace prepending. Return a new selector string.
  rw: selector => `.app ${selector}`,
  // `decl` runs on every emitted `prop: value` pair (top-level decls, decl
  // bodies under at-rules, and decl bodies under nested rules). Return
  // `{ prop, value }` to rewrite the pair, or `undefined` to leave it
  // unchanged. Each plugin in a chain composes left-to-right.
  decl: (prop, value) => {
    if (prop === 'cursor' && value === 'pointer') {
      return { prop, value: 'default' };
    }
    return undefined;
  },
};

The name field is required and contributes to the compiler's cache hash, so different plugin sets across nested <StyleSheetManager> trees stay isolated. Plugins without a name throw a styled-components error.

Legacy stylis plugins (those exporting a single function) don't run in v7 because they don't expose rw or decl. Production builds ignore them; development builds warn once for any plugin whose name is not recognized. Migrate them to the SCPlugin interface above; complex use cases that need a richer hook surface should open an issue.