Static CSS-in-JS research snapshot

vanilla-extract vs StyleX vs Pigment CSS

A focused comparison for teams choosing a static styling system in 2026, with Emotion kept as the runtime composition control so the tradeoffs stay visible.

Typed style sourcestylex.create / .css.ts / sx
Build passVite+ + prerender
Browser outputHTML + static CSS assets
01

Three static contracts

vanilla-extract optimizes for typed CSS artifacts, StyleX for atomic output and deterministic application order, and Pigment CSS for MUI-like authoring under build-time extraction.

02

Composition is the real fork

vanilla-extract composes classes while authoring styles. StyleX composes at the element boundary, where later applied styles override earlier ones even across component props.

03

Pigment CSS is a risk call

Its technical idea is relevant for MUI and RSC pressure, but MUI's 2026 roadmap says the project remains alpha and currently on hold.

04

This page now practices the claim

The shipped page is prerendered HTML with StyleX-extracted static CSS. Only the playground controls use a small enhancer after load.

Live demos

Change the inputs and watch the model behave

These are simplified simulations, not package sandboxes. Read them like a small lab: each control turns on a style instruction, and the preview shows which instruction gets the final say.

Style instructions are like rules

Base card, danger variant, density, and caller override are competing instructions for the same component.

The badge tells you who won

When the border color or spacing changes, the badge and notes explain why that rule beat the others.

Static CSS has a pressure point

Known choices are easy to prebuild. Truly unknown user input needs a variable bridge, wrapper, or runtime styling.

Demo 1

Composition winner

This demo asks a plain question: when several rules disagree about the same card, which rule gets the final word?

Caller override wins
Tool model
State
Density
Instruction stack

Solid rows are active. The preview shows their combined result.

Base cardDanger variantDensityCaller override
Final component previewPayment failed

Watch the border and spacing. They are the visible proof of which style instruction won.

Plain English

Think of this like transparent labels on one box. If two labels both say border color, StyleX keeps the later label for that one property without making new CSS.

What changed

The caller override is on, so the preview uses the caller color (#7c3aed) for the final border.

Problem to watch

Risk: the override model is strong, but authoring must stay inside StyleX compiler rules and supported dynamic patterns.

Technical note: The component default can be applied first, then the caller style last. The callsite makes the winner visible.

Demo 2

Dynamic brand color

This demo separates a fixed menu choice from an open-ended user choice. Static CSS loves the menu; arbitrary input needs a bridge.

User input pressure
Fixed menu

The token select is like choosing from approved paint chips. Static tools can prebuild those choices with confidence.

Open request

The color picker is like a customer writing any color on the form. Static CSS cannot preprint every possible answer, so the value must travel through a CSS variable, inline style, or runtime CSS.

vanilla-extract

Blue token

Strongest when the color is an approved token or theme contract, not a random value from the user.

StyleX

Variable bridge

Good when dynamic values are explicit and limited enough to flow through variables.

Pigment CSS

CSS variable wrapper

Useful for MUI-shaped code, but truly unknown values still need a wrapper or variable path.

Emotion

Direct runtime style

Most natural when arbitrary live values are core to the product, because it can style at runtime.

StyleX mechanics

How StyleX keeps static CSS but still composes well

The important distinction is that StyleX avoids runtime CSS generation, not every byte of runtime JavaScript. It prebuilds atomic CSS, then uses deterministic property-level merging when styles cross component boundaries.

01

Compile to atomic property classes

StyleX does not keep one opaque class for a whole style object. The compiler breaks styles into property-level CSS rules, so color, padding, border, and display can be reasoned about separately.

The browser receives static CSS. The app receives compiled references that point to prebuilt classes.
02

Merge by property at the callsite

stylex.props is not just string concatenation. It merges compiled style references so the last value for each CSS property wins, independent of where the styles were originally declared.

Component defaults can come first, feature state next, and caller style last. The final override policy is visible in one line.
03

Keep dynamic values explicit

When a value is not known at build time, StyleX can route it through a dynamic style function. The compiler emits a CSS variable reference, and runtime writes the value through the style prop.

No runtime stylesheet injection is needed, but there is still a small runtime bridge for the unknown value.
04

Erase the easy cases

For local, static usage, StyleX can compile away both stylex.create and stylex.props calls. More advanced composition keeps the small merge helper.

That is why the right phrase is zero runtime CSS generation, not always zero runtime JavaScript.

Limitation comparison

StyleX restrictions versus Emotion flexibility

StyleX moves friction to authoring and build time. Emotion keeps more runtime freedom, which is why it remains the useful control group for composition.

Arbitrary runtime values

Allowed only through explicit dynamic style functions or variables, and the docs say to use dynamic styles sparingly.

Directly ergonomic: object styles, template strings, and css props can read runtime values in normal component code.

StyleX protects static output and bundle discipline. Emotion protects authoring freedom.

Object spread and imported style objects

Constrained because the compiler must statically understand the style graph. Many runtime CSS-in-JS patterns become invalid.

Usually fine because Emotion resolves the object graph while JavaScript runs.

StyleX catches non-static patterns early. Emotion accepts more patterns, but moves work to runtime.

Caller overrides

Strong: pass caller style last to stylex.props and the last value wins by property, across files and component boundaries.

Also strong: css arrays and Emotion-generated className precedence make override composition convenient.

Both have good DX. StyleX keeps prebuilt CSS; Emotion pays with runtime style generation and insertion.

Shorthand and longhand collisions

Designed for deterministic merging even when properties overlap, such as margin versus marginTop.

Works naturally inside generated CSS, but the resolution follows generated rule order and runtime insertion behavior.

StyleX makes collision policy part of the style API. Emotion keeps the model closer to ordinary generated CSS.

Failure mode

Compilation fails or styles must be rewritten when code is too dynamic for static analysis.

The code usually runs, but the cost can appear as runtime style work, insertion ordering issues, or harder static optimization.

StyleX front-loads friction into authoring and build. Emotion defers more decisions to the browser session.

Plain language layer

If you do not write frontend code

The shortest version: these tools help teams decide how a page should look. The difference is when those decisions are finalized, which values are allowed to change later, and how much project risk comes with the styling engine.

Static CSS-in-JS

Like printing all store signs before opening the shop, instead of writing signs while customers are already inside.

The goal is to finish most visual work during build time, then ship normal CSS to the browser.

Composition

Like stacking several instruction sheets for the same object. If two sheets disagree, the important question is which sheet wins.

StyleX and Emotion make the winning order obvious at the callsite. vanilla-extract prefers defining the finished class ahead of time.

StyleX trick

Like giving every visual rule its own small label, then letting the checkout counter keep only the newest label for each category.

StyleX can preprint the labels as CSS, then do a tiny runtime merge to decide which preprinted class wins.

MUI migration

Like keeping a familiar dashboard layout while changing the engine under it.

Pigment CSS matters because it tries to keep MUI styled/sx habits while moving style work to build time.

Runtime values

A restaurant can print the menu before dinner, but cannot preprint every customer custom order.

Static tools handle known choices well. Arbitrary user-picked colors or CMS values often need CSS variables or a runtime styling tool.

Alpha/on hold

A promising road that was surveyed but is not currently being extended.

Pigment CSS ideas are useful to study, but current adoption should treat project status as a first-class risk.

Visual mental model

Where the styling work happens

vanilla-extract

Write typed CSS in .css.ts files, usually near tokens, recipes, and package-level design-system code.

The bundler evaluates the style modules and emits CSS classes, variables, themes, and optional helper mappings.

The browser receives ordinary CSS assets and class names. Core styles do not need runtime style injection.

None for core styles

StyleX

Write stylex.create objects and pass compiled style references through stylex.props at component boundaries.

The compiler extracts atomic CSS rules and replaces authoring objects with compiled references.

A small helper merges the applied styles into final className and inline variable output.

Tiny merge helper

Pigment CSS

Write css, styled, variants, and sx in a MUI/Emotion-like style, with static extraction constraints in mind.

The Pigment plugin intercepts supported styling APIs and extracts them into CSS during the framework build.

The page uses generated CSS, className, and style props for CSS-variable bridges instead of runtime style generation.

CSS variable bridge

Emotion

Write css objects, template strings, arrays, and component-local dynamic styles in normal React code.

The build ships Emotion runtime code plus application styling logic unless separate extraction tooling is added.

Styles are generated, merged, and inserted while JavaScript runs, making arbitrary dynamic styling easy.

Style generation

Concrete situations

Four examples that explain the tradeoff

Everyday version

A product button has base, size, and danger states

A button starts with house rules, chooses a size, and may become a warning button.

Base buttonSmall sizeDanger tone

The base says neutral border. The danger state says red border. The final button should be red.

vanilla-extract
Best expressed as a Recipe: base styles plus typed variants for known button states.
StyleX
Best expressed as callsite composition: base first, size next, danger last.
Pigment CSS
Good fit with styled variants when the prop values are known and extractable.
Emotion
Very ergonomic with css arrays: [base, small, danger], resolved at runtime.

Everyday version

A page theme changes from light to dark

The same room gets a different lighting plan, but the furniture does not need to be rebuilt.

Token contractTheme classComponent styles

Components should keep using the same token names while the token values change under a parent theme.

vanilla-extract
Strong fit: createTheme and createThemeContract make typed token contracts and theme classes.
StyleX
Strong fit with defineVars and compiler-aware theme variables.
Pigment CSS
Good MUI fit through plugin-configured themes, extendTheme vars, and color schemes, but runtime theme use is discouraged.
Emotion
Comfortable with ThemeProvider or CSS variables, but common theming relies on runtime React context.

Everyday version

A reusable card lets the caller override spacing

A component ships with a default layout, but a page can tighten it for a specific use.

Card defaultPage contextCaller override

The card default says padding 24. The caller says padding 12. The caller should win without changing the card source.

vanilla-extract
Possible, but core class composition does not automatically give callsite-last semantics.
StyleX
Native fit: pass props.style last to stylex.props and the caller override wins by application order.
Pigment CSS
Possible with sx arrays or wrapper conventions when the override is extractable; arbitrary runtime values need CSS variables.
Emotion
Native fit: css arrays make later caller styles override earlier defaults.

Everyday version

A user chooses a custom brand color in a live editor

The product cannot know every customer color before the page opens.

Static tokenUser inputCSS variable

A static build wants known values, but the customer can pick any hex color at runtime.

vanilla-extract
Use a theme contract and assign CSS variables at runtime for the unknown value.
StyleX
Use dynamic style functions or variables, while keeping the shape compiler-friendly.
Pigment CSS
Move the runtime color into a CSS variable or inline style wrapper; direct runtime sx values are not extractable.
Emotion
Directly accepts the runtime value inside css or object styles.

Pseudo-real case lab

Where each tool wins or starts to hurt

These are fictional but realistic product situations. The point is to make each tool's advantage and failure mode visible without pretending one answer is always correct.

Northwind Ops Console

A SaaS dashboard design system with 80 components

vanilla-extract fit

Setup

A B2B team maintains tables, filters, alerts, drawers, billing screens, and admin tools across multiple product squads.

Pressure

Design wants strict tokens and approved variants. Product squads want to ship without inventing one-off button, badge, and spacing rules.

Why this case matters

This highlights vanilla-extract because most choices are known before runtime: tokens, recipes, variants, compound states, and CSS output.

Visible win

Recipes make supported states explicit. Reviewers can see whether a component uses approved sizes and tones.

Visible risk

When a page wants a one-off override, the team needs a variant, class convention, or wrapper.

vanilla-extract95/100

Best for typed design-system contracts and static variant surfaces.

StyleX78/100

Good if React composition and override order become central.

Pigment CSS56/100

Only compelling here if the system is already deeply MUI-based.

Emotion60/100

Flexible, but runtime styling is not the main goal for this stable system.

Launchpad Creator Feed

A reusable feed card that appears in ten contexts

StyleX fit

Setup

The same card appears in home feed, search results, promoted placements, compact sidebars, and partner embeds.

Pressure

Each context wants small differences: tighter padding, hidden metadata, different emphasis, or local color accents.

Why this case matters

This highlights StyleX because the important question is which caller is allowed to override which property.

Visible win

stylex.props can place component defaults first and caller style last. The winner is visible at the element boundary.

Visible risk

Teams must accept StyleX static-analysis rules. Arbitrary object spreading and imported dynamic style values become migration pain.

vanilla-extract68/100

Good for named variants, weaker when every caller needs local final say.

StyleX96/100

Best for deterministic property-level overrides across component boundaries.

Pigment CSS63/100

sx can help MUI consumers, but extraction limits still shape the API.

Emotion86/100

Also strong for callsite overrides, but with runtime style generation.

Meridian Admin Suite

A Material UI app migrating away from Emotion pressure

Pigment CSS fit

Setup

A mature MUI app has hundreds of Box sx props, styled components, theme overrides, and Next.js App Router adoption pressure.

Pressure

The team wants fewer runtime style costs and better RSC alignment, but does not want to rewrite every component into a new styling model.

Why this case matters

This highlights Pigment CSS because it is designed around MUI migration: @mui/material-pigment-css, transformLibraries, sx, styled, and theme config.

Visible win

Developers can keep familiar MUI authoring patterns while moving supported styles into build output.

Visible risk

The current alpha/on-hold status, pnpm caveat, and runtime-value migration rules make it risky as a default platform bet.

vanilla-extract58/100

Good static target, but not a MUI semantics migration path.

StyleX61/100

Technically strong, but would be a larger architecture migration.

Pigment CSS84/100

Best functional fit for this specific migration, with explicit adoption risk.

Emotion70/100

Keeps today working, but does not solve the static/RSC pressure.

Signal Campaign Studio

A campaign builder with user-picked colors and live previews

Emotion fit

Setup

Marketers drag blocks, choose brand colors, preview layouts, and save experiments while the page updates instantly.

Pressure

Many values come from user input, CMS fields, or preview-only combinations that are not known at build time.

Why this case matters

This highlights Emotion because runtime styling is not a weakness here; it is the reason the experience is easy to build.

Visible win

css arrays and dynamic objects can directly reflect user input. The implementation stays close to live preview state.

Visible risk

If prototype code hardens into production, temporary dynamic styles can become the design system by accident.

vanilla-extract54/100

Useful for the editor shell, but not ideal for arbitrary user-generated styling.

StyleX58/100

Can use variables, but arbitrary dynamic objects fight the compiler model.

Pigment CSS45/100

Runtime values must be reworked into CSS variables or wrapper styles.

Emotion95/100

Best for highly dynamic preview surfaces and runtime style composition.

Atlas Launch Pages

A public marketing site plus an embedded app surface

vanilla-extract fit

Setup

A team ships docs pages, landing pages, pricing pages, and a small logged-in widget from one shared design language.

Pressure

The public site wants static assets, predictable CSS, and low JavaScript. The widget still needs component-level consistency.

Why this case matters

This highlights vanilla-extract because it treats styling as build output while keeping design tokens in TypeScript.

Visible win

The marketing site gets static CSS and shared token contracts without adopting a React-specific styling mental model everywhere.

Visible risk

If the embedded widget later becomes override-heavy, StyleX may fit that widget better than the shared site styling system.

vanilla-extract92/100

Best when cross-framework static output and typed tokens matter together.

StyleX72/100

Strong inside the React widget, less compelling for non-React surfaces.

Pigment CSS48/100

Hard to justify unless the pages are already MUI-centered.

Emotion52/100

Convenient, but adds runtime styling where static output is the product goal.

LedgerFlow Back Office

A legacy React app with many hand-written className overrides

StyleX fit

Setup

An older internal app has dozens of components accepting className, style, and theme props from different product teams.

Pressure

Nobody fully trusts CSS order. A small change in one shared component can change spacing or colors in a distant screen.

Why this case matters

This highlights StyleX because deterministic composition directly answers "I passed an override, why did it not win?"

Visible win

The migration can make override order explicit: component defaults, feature state, then caller style.

Visible risk

Legacy dynamic patterns may need cleanup before they compile. The migration may be architectural, not a search-and-replace.

vanilla-extract66/100

Good for rebuilding stable components, less direct for override-order cleanup.

StyleX91/100

Best when override determinism is the migration goal.

Pigment CSS57/100

Useful only if the legacy app is specifically MUI/Emotion-based.

Emotion83/100

Practical for incremental migration, but keeps runtime styling in place.

Checkout Experiment Room

A one-off prototype that changes every design review

Emotion fit

Setup

A product team is testing checkout flows, layouts, and color treatments before committing to a design-system change.

Pressure

The layout changes every week. Most style decisions are temporary, and developer speed matters more than long-term static output.

Why this case matters

This highlights Emotion because its flexibility is valuable before the team knows which abstractions deserve to become permanent.

Visible win

Developers can compose temporary states directly in the component and delete them later without designing a full variant API.

Visible risk

If prototype code becomes production code, runtime styling can become the accidental design system.

vanilla-extract62/100

Useful once the experiment stabilizes, heavier while ideas are volatile.

StyleX64/100

Good if the prototype already follows final app constraints.

Pigment CSS50/100

Too much migration risk unless the prototype is testing MUI migration itself.

Emotion93/100

Best for speed and exploratory composition before rules stabilize.

Comparison matrix

Practical differences by decision surface

Current status

Established zero-runtime CSS-in-TypeScript project with official integrations and companion packages such as Recipes and Sprinkles.

Actively developed Meta styling system with static atomic CSS, compiler plugins, and recent public docs around scale and composition.

MUI docs still mark it as early alpha, and the January 2026 MUI roadmap says Pigment CSS is currently on hold.

Pigment CSS should be evaluated as a promising MUI migration experiment, not as the same adoption-risk class as vanilla-extract or StyleX.

Core model

Author styles in .css.ts files. The bundler turns TypeScript objects, variables, themes, and recipes into regular CSS assets.

Author stylex.create objects. The compiler emits atomic CSS, and components apply compiled references with stylex.props.

Author with Emotion-like css, styled, variants, and sx APIs. The Pigment plugin extracts supported styles into CSS at build time.

vanilla-extract feels like typed CSS Modules. StyleX feels like an atomic compiler. Pigment CSS feels like a build-time MUI/Emotion adapter.

Runtime cost

Core style output does not generate or inject CSS in the browser. Optional helpers may select prebuilt classes.

No runtime style injection. A small merge helper remains for non-trivial composition, and the compiler can remove create/props calls when usage is local and static.

Targets zero-runtime CSS extraction, but dynamic escape hatches move values through CSS variables or inline style props.

All three static candidates reduce browser-side style generation. StyleX is better described as zero runtime CSS generation, not zero JavaScript in every path.

Setup and framework fit

Official integrations cover Vite, Next.js, Webpack, Rollup, esbuild, Parcel, Astro, Gatsby, Remix, and more.

Best documented for React-heavy apps, with Vite, webpack/Rspack, esbuild, Babel, and PostCSS paths.

Official MUI migration docs list Vite and Next.js App Router with webpack v5; Turbopack is not supported yet, and a pnpm plugin issue is called out.

For a generic Vite app, vanilla-extract has the broadest low-risk fit. For MUI migration, Pigment CSS is relevant but carries toolchain constraints.

Composition

style([...]) composes while defining classes. It can combine class names and style objects into a reusable classlist.

stylex.props composes at the element boundary by merging compiled atomic property references. The last applied style wins regardless of declaration order.

Composition uses familiar styled/css/sx surfaces, but the compiler must be able to extract the result. sx supports arrays, theme callbacks, and build-time replacement.

StyleX has the clearest callsite override story. vanilla-extract has the cleanest build-time class composition. Pigment CSS keeps familiar MUI ergonomics but adds extraction limits.

Variants and props

Recipes gives an explicit, type-safe API for base styles, variants, compound variants, and defaults.

Variants are usually plain TypeScript lookups and conditionals passed into stylex.props.

styled supports a variants key for build-time-known props, which maps well to MUI-style component variants.

vanilla-extract is strongest for formal design-system variants. Pigment CSS is comfortable for MUI-like variants. StyleX keeps variant logic closest to component code.

Dynamic values

Unknown runtime values generally need CSS variables through a contract or inline assignment; createTheme itself generates static CSS.

Dynamic style functions are supported for simple runtime values, usually compiled to CSS variable references plus inline style output. The docs call this an advanced feature to use sparingly.

MUI migration docs explicitly say runtime-dependent dynamic values cannot be extracted and should be moved to CSS variables or inline style wrappers.

Emotion is still the easiest model for arbitrary live values. Among static tools, StyleX has the most explicit dynamic function path; Pigment CSS requires the most migration discipline.

Override mechanics

Overrides are safest when modeled as named variants or authored composition. A random caller class still depends on CSS order and specificity.

Atomic output lets StyleX merge by CSS property rather than by opaque class string, so caller-last overrides stay predictable across component boundaries.

sx arrays and styled variants keep familiar MUI override ergonomics, but only extractable shapes can move into static CSS.

This is StyleX's special advantage over many static tools: the override decision is a runtime class-selection problem, not runtime CSS generation.

Theming

createTheme and createThemeContract define strongly typed CSS variable contracts and multiple theme classes.

defineVars and theme variables are compiler-aware, with strict file rules that make token surfaces explicit.

Themes are configured in the plugin; extendTheme can generate vars and color schemes, but runtime theme usage is discouraged unless necessary.

All three can use CSS variables well. Pigment CSS is most tied to MUI theme migration and precompiled theme shape.

CSS expressiveness

Supports selectors, pseudos, media, supports, container queries, keyframes, font-face, layers, and global styles in typed objects.

Optimizes around atomic property rules, pseudos, conditions, variables, and static constraints rather than arbitrary CSS object freedom.

Aims to preserve CSS-in-JS ergonomics with css, styled, keyframes, globalCss, sx, and nested selector support at build time.

vanilla-extract is closest to writing typed CSS. StyleX is deliberately constrained for scale. Pigment CSS tries to keep Emotion-like authoring while extracting.

MUI migration

Can be used beside MUI, but it does not try to preserve MUI styled/sx semantics or transform MUI internals.

A separate styling system. Migrating from MUI Emotion styles would be an architectural migration, not a drop-in path.

Designed specifically to integrate with Material UI through @mui/material-pigment-css and transformLibraries.

Pigment CSS has the unique MUI migration story. That story is useful, but the project status makes it a cautious bet.

Failure mode

Teams may over-create variants or need conventions when a caller wants final say over a single property.

Compiler restrictions can block imported dynamic values, object spreads, or patterns that were normal in runtime CSS-in-JS.

Build-time extraction can fail on runtime state, ownerState callbacks, pnpm/tooling edge cases, or unsupported framework paths.

The question is not just "can it style this?" It is "where will the team feel friction after the first migration week?"

Best default

Best default for typed design systems, cross-framework packages, stable variants, and predictable static CSS.

Best default for React apps where property-level composition and caller override order are central.

Best candidate only when the problem is specifically MUI/Emotion migration and the alpha/on-hold risk is acceptable.

If starting fresh today, choose vanilla-extract or StyleX first. Reach for Pigment CSS only with a concrete MUI migration reason and a fallback plan.

Example cookbook

Code patterns side by side

These are intentionally small examples. They are not exhaustive production snippets; they are meant to make the composition model visible.

Button variants

This is the design-system case: a small set of supported combinations should be easy to type, audit, and reuse.

vanilla-extract

export const button = recipe({
  base: { borderRadius: 6, fontWeight: 700 },
  variants: {
    tone: { danger: { background: vars.color.danger } },
    size: { sm: { padding: '6px 10px' } },
  },
})

button({ tone: 'danger', size: 'sm' })

The API advertises allowed variants and keeps generated output static.

StyleX

const styles = stylex.create({
  base: { borderRadius: 6, fontWeight: 700 },
  danger: { backgroundColor: 'red' },
  small: { padding: '6px 10px' },
})

stylex.props(styles.base, isDanger && styles.danger, styles.small)

The component decides the final style stack at the element boundary.

Pigment CSS

const Button = styled('button')({
  borderRadius: 6,
  fontWeight: 700,
  variants: [
    { props: { tone: 'danger' }, style: { background: 'red' } },
    { props: { size: 'sm' }, style: { padding: '6px 10px' } },
  ],
})

The MUI-like variants shape is familiar, but values must stay extractable.

Emotion

const base = css({ borderRadius: 6, fontWeight: 700 })
const danger = css({ backgroundColor: 'red' })
const small = css({ padding: '6px 10px' })

<button css={[base, isDanger && danger, small]} />

The composition reads naturally, but style resolution is runtime work.

Caller overrides

This is the component-library case: consumers need a controlled way to adjust one component without fighting CSS order.

vanilla-extract

export const card = style({ padding: 24 })
export const compactCard = style([card, { padding: 12 }])

<section className={compactCard} />

Excellent when the override is known while authoring the style module.

StyleX

function Card(props) {
  return <section {...stylex.props(styles.card, props.style)} />
}

<Card style={styles.compact} />

Excellent when the caller should decide final overrides.

Pigment CSS

function Card(props) {
  return <section sx={[cardSx, props.sx]} />
}

<Card sx={{ padding: 12 }} />

Familiar for MUI teams, as long as the sx values are extractable or variable-backed.

Emotion

function Card({ css: cssProp }) {
  return <section css={[card, cssProp]} />
}

<Card css={compact} />

The caller override pattern is very direct, with runtime flexibility.

User-picked color

This is the runtime-value case: the value is not just conditional, it is unknown until a person or server sends it.

vanilla-extract

export const brandColor = createVar()
export const badge = style({ color: brandColor })

<span
  className={badge}
  style={assignInlineVars({ [brandColor]: userColor })}
/>

Promote the dynamic value into a CSS variable contract.

StyleX

const styles = stylex.create({
  badge: (color) => ({ color }),
})

<span {...stylex.props(styles.badge(userColor))} />

Dynamic functions make the variable bridge explicit.

Pigment CSS

const Badge = styled('span')({
  color: 'var(--brand-color)',
})

<Badge style={{ '--brand-color': userColor }} />

Move the unknown value to a CSS variable instead of direct runtime sx.

Emotion

<span css={{ color: userColor }} />

// Direct and ergonomic because Emotion can generate CSS at runtime.

Best ergonomics when arbitrary runtime values are the product requirement.

Who wins when styles conflict?

Most styling bugs are not about setting a value. They are about understanding which value wins when two values collide.

vanilla-extract

const base = style({ color: 'gray' })
const danger = style([base, { color: 'red' }])

// The authored composition owns the result.

The safest path is to compose the final class in the style module.

StyleX

stylex.props(styles.base, styles.primary, styles.danger)

// danger wins because it is applied last.

The winning rule is visible where the element is rendered.

Pigment CSS

<Button sx={[baseSx, primarySx, dangerSx]} />

// Familiar order, but only supported style shapes can be extracted.

The model is ergonomic, but extraction errors are the hidden failure mode.

Emotion

<Button css={[base, primary, danger]} />

// Later Emotion styles win at runtime.

The winning rule is also visible at the callsite, with runtime insertion.

Composition deep dive

What "compose" means in each model

Build-time classlist composition

vanilla-extract

Composition happens while authoring styles. style([...]) accepts existing class names and style objects, then returns a classlist that consuming code treats as a single exported class.

Strengths

  • Excellent for reusable design-system primitives.
  • Composed classes can still participate in selectors as a single generated identity.
  • Deep merging style objects is useful for typed style utilities.

Caveats

  • Callsite overrides are still ordinary class and rule-order behavior unless the team adds a convention.
  • Runtime conditional composition usually happens through class joining, Recipes, Sprinkles, or wrapper components.
const base = style({ padding: 12 })
const primary = style([base, { background: 'blue' }])

// primary is a reusable classlist string
// built before the browser runs.
Official composition source

Callsite-first deterministic composition

StyleX

Composition happens where the element is rendered. stylex.props receives styles, arrays, falsy conditionals, and style props, then merges by application order.

Strengths

  • The last applied style wins, independent of declaration order.
  • Nested arrays and style props work naturally across component boundaries.
  • A component can put defaults first and caller styles last so the override policy is visible.

Caveats

  • The merge helper is part of the runtime model when the compiler cannot erase the call, so it is not literally zero JavaScript.
  • Static analyzability rules make arbitrary imported style objects, spreads, and direct runtime style values invalid.
  • Dynamic style functions are available, but StyleX documentation frames them as advanced and sparing-use.
<div
  {...stylex.props(
    styles.base,
    isHighlighted && styles.highlighted,
    props.style,
  )}
/>
Official composition source

MUI-like composition under extraction rules

Pigment CSS

Composition uses familiar css, styled, variants, and sx APIs. The difference from Emotion is that Pigment CSS must extract supported style shapes during the bundler pass.

Strengths

  • Preserves a familiar MUI/Emotion-style authoring surface.
  • styled variants are comfortable when props are known at build time.
  • sx can be used on Material UI components, HTML elements, and third-party JSX components after setup.

Caveats

  • Runtime-dependent values must be moved to CSS variables or inline style wrappers.
  • The project is alpha and currently on hold according to MUI 2026 roadmap communication.
const Heading = styled('h1')({
  fontSize: '2rem',
  variants: [
    { props: { tone: 'danger' }, style: { color: 'red' } },
  ],
})

<div sx={[baseSx, compactSx]} />
Official composition source

Runtime composition and insertion-order control

Emotion

Emotion is not the static zero-runtime candidate here, but it is the useful control group: css arrays and Emotion-generated class names compose in the order you use them.

Strengths

  • Very ergonomic for arbitrary dynamic values, object styles, template strings, and React css props.
  • Composition order is explicit at the callsite.
  • It is practical for prototypes, CMS-driven styling, and live previews.

Caveats

  • It generates and inserts styles at runtime in the usual setup.
  • It can accept arbitrary dynamic values more directly, but that flexibility is the runtime work static tools avoid.
  • Composition is very ergonomic, but it does not provide StyleX-style static analyzability guarantees.
<div css={[base, danger]}>Red wins</div>
<div css={[base, cssPropFromCaller]} />
Official composition source

Recommendation

Decision guide

Choose vanilla-extract when

You are building a typed design system, need explicit variant APIs, want framework portability, and prefer CSS-module-like build artifacts.

Choose StyleX when

Your app is React-heavy, component reuse is deep, override order matters at the element boundary, and strict compiler rules are acceptable.

Treat Pigment CSS as a migration bet when

You are specifically evaluating a Material UI Emotion migration and can accept early-alpha, currently-on-hold project risk with a fallback path.

Keep Emotion when

You need high dynamic styling flexibility, live preview ergonomics, or a prototype surface where runtime style generation is the feature, not the problem.

Evidence

Versions and primary sources