styled-components v7
What's new in v7
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.
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.
@mediaand@containerqueries, 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 instyled.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/:disabledpseudo-states. - Native animations by default.
transition,@keyframes, and@starting-stylerun on React Native through the built-inAnimated-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-webbuild.styled-components/nativeships a smaller bundle for rn-web targets that defers to the browser forlight-dark()+prefers-color-schemerepaints, distinctdvh/svh/lvh/vi/vbresolution, wide-gamutoklch/oklab/lch/lab/color-mix(), and paint-timecalc()/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 replacesstylis-plugin-rtl; custom plugins move to a narrowerSCPlugininterface. See Plugins moved to a dedicated subpath. extractCSS()reads the current stylesheet as plain text. Replaces the legacydisableCSSOMInjectiontoggle.- Global styles emit once. Mounting the same
createGlobalStylecomponent 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'stintColor, icon libraries). Details → - Faster SSR at scale.
Peer dependency floors
reactandreact-dom≥ 19.0.0react-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
reactandreact-domto^19.react-nativeto^0.85if you use it.react-native-reanimatedto^4if you opt into the native animation adapter.- Drop
css-to-react-nativeif it was in yourpackage.jsononly 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: nonenow emitsborder-style: none(previouslysolid, which produced surprising hairlines).- The optional reanimated adapter is opt-in at app entry:
import 'styled-components/native/reanimated'.react-native-reanimated@^4becomes a peer dependency only in that case. The adapter is experimental and not heavily tested yet; the defaultAnimated-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
IInlineStyleandIInlineStyleConstructorwere renamed toINativeStyleandINativeStyleConstructor. The old names are exported as deprecated aliases.Stringifieris exported as a deprecated alias for the newCompilerinterface (the shape differs, so consuming code that called the type as a function will need to migrate toCompiler.compile/Compiler.emit).FlattenerandNameGeneratorare 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). Constantspianderesolve 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>, …)throughsrgb,oklab,oklch,lab, orlch.- CSS Color 4 system color keywords auto-switch with OS appearance:
canvas,canvastext,field,fieldtext,graytext,highlight,highlighttext,linktext,visitedtext,activetext. Usable insidebox-shadow,filter: drop-shadow(),background, andlinear-gradientcolor stops. - Viewport units:
vw,vh,vi,vb,vmin,vmax, plus thes*/l*/d*variants (dvh,svw,lvi). - Container query units:
cqw,cqh,cqmin,cqmax. - Font-relative units:
rem,em,lh,rlhresolve 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 fullborder-inline/border-blockfamily. line-clamp: Ntruncates<Text>to N lines.text-wrap: nowrapcollapses to a single line;text-wrap-style: balance/prettyimprove line-breaking on Android.text-align: start | end | match-parentresolves correctly under RTL on both platforms.direction: ltr | rtlcontrols<Text>bidi.aspect-ratiowith bare ratio, slash form,auto, and the two-valueauto <ratio>form.transform:matrix(...),matrix3d(...), and bare numbers intranslateX(10). Top-levelperspective: 1000pxworks without folding it into a transform array.font-style: obliquefalls back to italic;font-familyrecognizes the 13 generic CSS keywords (system-ui,ui-*,sans-serif,serif,monospace,cursive,fantasy,emoji,math,fangsong).font-sizeaccepts 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,icandr-prefixed), and absolute lengths (pt,pc,in,cm,mm,Q).letter-spacingacceptsem,rem,lh, andrlh.place-itemsandplace-selffor the align axis.field-sizing: contentauto-grows a<TextInput>to its content.interactivity: inertsuppresses interaction and hides the subtree from screen readers.text-overflow: ellipsis | cliptruncates<Text>once a line limit is set. Pair withline-clamp: N(ortext-wrap: nowrapfor one line); without a line limit RN has no line to ellipsize and the value is a no-op.overscroll-behavior: contain | nonedisables overscroll onScrollView/FlatList.scrollbar-width: nonehides scroll indicators.accent-colortints<Switch>(autopicks up the platform accent color).styled.ScrollViewon native defaults toflex-shrink: 0so explicitwidth:/height:pin reliably inside a flex parent. Override withflex-shrink: 1if you need it.- CSS custom properties.
--name: valuedeclarations 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: initialresets). 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!importantshorthand propagates to every longhand. Importance flows throughvar()substitution and render-time resolvers. Web-aligned: a styled!importantbeats a runtimestyle={{ ... }}prop.!importantinside@keyframesis 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/::selectionbackdrop-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-pathposition: fixed,position: sticky,position: anchor()/anchor-nametext-indent,word-spacing,text-box-trim,text-spacing-trimtransform-style: preserve-3d(basicrotateX/rotateY/translateZ/perspectivedo work)- CSS Grid (
display: grid,grid-template-*) - Form-state pseudos (
:invalid,:required,:read-only,:checked) @supportsis parsed but its condition is never evaluated against the runtime, so declarations inside always apply on native. Branch withPlatform.OSinstead.- Scroll-snap, view transitions, scroll-driven animations
Platform-conditional features (the matrix has the full story):
filter: onlybrightnessandopacityapply on iOS without the SwiftUI opt-in (setReactNativeReleaseLevel: experimentalinInfo.plist/ios.infoPlist); Androidbluranddrop-shadowrequire API 31+.mix-blend-mode: all 15 non-normalmodes 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-left↔padding-right,margin-left↔margin-right,border-left↔border-right, etc.). - Flips
left/rightkeyword values onfloat,clear,text-align, andcaption-side. - Mirrors 4-value shorthand positions (
margin: 0 4px 0 8px;becomesmargin: 0 8px 0 4px). - Logical properties like
margin-inline-startpass 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.