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 and @container queries, viewport and container-query units, font-relative units, modern color spaces, relative-color syntax, the full Math L4 family, logical shorthands, gradients, and filters all work in styled.View\...``. See React Native gets modern CSS for the full surface and the compatibility matrix for per-feature status.
  • Selectors and combinators on React Native. Attribute selectors with every operator, :not(<simple>), :has(<simple>), tree-structural pseudos, the four combinators between styled-component references, and the :hover / :focus / :pressed / :disabled pseudo-states.
  • Native animations by default. transition, @keyframes, and @starting-style run on React Native through the built-in Animated-based adapter, with no setup or peer dependency. An optional reanimated adapter is available behind one import (experimental; not heavily tested yet).
  • createTheme() on native, with the same contract as web. <ThemeProvider> deep-merges nested themes so an inner override that touches one leaf keeps the siblings it inherited.
  • Dedicated react-native-web build. styled-components/native ships a smaller bundle for rn-web targets that defers to the browser for light-dark() + prefers-color-scheme repaints, distinct dvh / svh / lvh / vi / vb resolution, wide-gamut oklch / oklab / lch / lab / color-mix(), and paint-time calc() / clamp() / min() / max() against the real containing block. Auto-detected by Webpack, Vite, and Metro web targets; no opt-in needed.
  • Plugins subpath. import { rtlPlugin, rscPlugin } from 'styled-components/plugins'. First-party RTL replaces stylis-plugin-rtl; custom plugins move to a narrower SCPlugin interface. See Plugins moved to a dedicated subpath.
  • extractCSS() reads the current stylesheet as plain text. Replaces the legacy disableCSSOMInjection toggle.
  • Global styles emit once. Mounting the same createGlobalStyle component multiple times now emits its CSS only once.
  • Remapping CSS into native props via the function form of .attrs((props, ast) => ...). Most useful for React Native libraries that style via props (e.g. react-native-svg's <Path fill="..." />, Image's tintColor, icon libraries). Details →
  • Faster SSR at scale.

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.

Plugins moved to a subpath

stylisPluginRSC and the v6 stylisPlugins prop are gone. Import the first-party plugins from styled-components/plugins and pass them via the renamed plugins prop:

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

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

stylis-plugin-rtl is replaced by the first-party rtlPlugin. Custom stylis plugins authored against the v6 middleware contract no longer load and must port to the new SCPlugin shape. See Plugins moved to a dedicated subpath for details and authoring guidance.

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 most common gotchas:

  • The peer floor moves to RN ≥ 0.85.
  • border: none now emits border-style: none (previously solid, which produced surprising hairlines).
  • The optional reanimated adapter is opt-in at app entry: import 'styled-components/native/reanimated'. react-native-reanimated@^4 becomes a peer dependency only in that case. The adapter is experimental and not heavily tested yet; the default Animated-based adapter runs without any setup.

React Native gets modern CSS walks through the new surface (modern color spaces, gradients, filters, selectors, animations, createTheme()) and calls out the platform-level caveats (iOS SwiftUI filter opt-in, hover feature flag, conic-gradient gap, and the rest). The compatibility matrix has the per-feature breakdown.

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. Usable inside box-shadow, filter: drop-shadow(), background, and linear-gradient color stops.
  • 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. direction: ltr | rtl controls <Text> bidi.
  • 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).
  • font-size accepts the full CSS keyword set (xx-small...xxx-large, smaller, larger) plus the full unit catalogue: viewport units (vh, vw, svh, dvh, vi, vb), container query units (cqh, cqw, cqi, cqb, cqmin, cqmax), font-relative (em, rem, lh, rlh), font-metric (ex, cap, ch, ic and r-prefixed), and absolute lengths (pt, pc, in, cm, mm, Q). letter-spacing accepts em, rem, lh, and rlh.
  • 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.
  • text-overflow: ellipsis | clip truncates <Text> once a line limit is set. Pair with line-clamp: N (or text-wrap: nowrap for one line); without a line limit RN has no line to ellipsize and the value is a no-op.
  • overscroll-behavior: contain | none disables overscroll on ScrollView / FlatList. scrollbar-width: none hides scroll indicators.
  • accent-color tints <Switch> (auto picks up the platform accent color).
  • styled.ScrollView on native defaults to flex-shrink: 0 so explicit width: / height: pin reliably inside a flex parent. Override with flex-shrink: 1 if you need it.
  • CSS custom properties. --name: value declarations cascade through styled descendants; var(--name, fallback) reads them back with the full CSS Variables L1 contract (fallbacks, nested resolution, cycle detection, case-sensitive names, quote-aware skip inside string values, --foo: initial resets). References resolve against the cascade inside every conditional bucket (@media, @container, @supports, attribute, pseudo-state, :has(), :nth-child(), combinator).
  • !important. Honored within the same component for base + every conditional bucket. The marker is stripped from the rendered value, beats normal declarations regardless of source order, and a !important shorthand propagates to every longhand. Importance flows through var() substitution and render-time resolvers. Web-aligned: a styled !important beats a runtime style={{ ... }} prop. !important inside @keyframes is ignored per CSS Animations. Cross-component cascade is not yet supported.

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 on RN ≥ 0.85, but stock RN gates them behind a feature flag that defaults to FALSE in 0.85. Flip it at app startup to get &:hover to fire:

import { ReactNativeFeatureFlags } from 'react-native';


ReactNativeFeatureFlags.override({
  shouldPressibilityUseW3CPointerEventsForHover: () => true,
});

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. :nth-child(<an+b> of S) and :nth-last-child(<an+b> of S) filter the count by a styled-component reference or a single attribute selector (same simple-inner forms :has() accepts). 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 (linear / radial gradients work; conic-gradient parses but does not paint on native)
  • 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)
  • @supports is parsed but its condition is never evaluated against the runtime, so declarations inside always apply on native. Branch with Platform.OS instead.
  • Scroll-snap, view transitions, scroll-driven animations

Platform-conditional features (the matrix has the full story):

  • filter: only brightness and opacity apply on iOS without the SwiftUI opt-in (set ReactNativeReleaseLevel: experimental in Info.plist / ios.infoPlist); Android blur and drop-shadow require API 31+.
  • mix-blend-mode: all 15 non-normal modes on iOS; Android needs API 29+.
  • transform: skewX/skewY: iOS renders correctly; Android silently drops the shear pending an upstream fix.

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.