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.
Static CSS-in-JS research snapshot
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.
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.
vanilla-extract composes classes while authoring styles. StyleX composes at the element boundary, where later applied styles override earlier ones even across component props.
Its technical idea is relevant for MUI and RSC pressure, but MUI's 2026 roadmap says the project remains alpha and currently on hold.
The shipped page is prerendered HTML with StyleX-extracted static CSS. Only the playground controls use a small enhancer after load.
Live demos
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.
Base card, danger variant, density, and caller override are competing instructions for the same component.
When the border color or spacing changes, the badge and notes explain why that rule beat the others.
Known choices are easy to prebuild. Truly unknown user input needs a variable bridge, wrapper, or runtime styling.
Demo 1
This demo asks a plain question: when several rules disagree about the same card, which rule gets the final word?
Caller override winsSolid rows are active. The preview shows their combined result.
Watch the border and spacing. They are the visible proof of which style instruction won.
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.
The caller override is on, so the preview uses the caller color (#7c3aed) for the final border.
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
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 pressureThe token select is like choosing from approved paint chips. Static tools can prebuild those choices with confidence.
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.
Strongest when the color is an approved token or theme contract, not a random value from the user.
Good when dynamic values are explicit and limited enough to flow through variables.
Useful for MUI-shaped code, but truly unknown values still need a wrapper or variable path.
Most natural when arbitrary live values are core to the product, because it can style at runtime.
StyleX mechanics
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.
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.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.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.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 moves friction to authoring and build time. Emotion keeps more runtime freedom, which is why it remains the useful control group for composition.
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.
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.
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.
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.
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
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.
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.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.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.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.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.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
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 stylesWrite 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 helperWrite 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 bridgeWrite 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 generationConcrete situations
Everyday version
A button starts with house rules, chooses a size, and may become a warning button.
The base says neutral border. The danger state says red border. The final button should be red.
Everyday version
The same room gets a different lighting plan, but the furniture does not need to be rebuilt.
Components should keep using the same token names while the token values change under a parent theme.
Everyday version
A component ships with a default layout, but a page can tighten it for a specific use.
The card default says padding 24. The caller says padding 12. The caller should win without changing the card source.
Everyday version
The product cannot know every customer color before the page opens.
A static build wants known values, but the customer can pick any hex color at runtime.
Pseudo-real case lab
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 B2B team maintains tables, filters, alerts, drawers, billing screens, and admin tools across multiple product squads.
Design wants strict tokens and approved variants. Product squads want to ship without inventing one-off button, badge, and spacing rules.
This highlights vanilla-extract because most choices are known before runtime: tokens, recipes, variants, compound states, and CSS output.
Recipes make supported states explicit. Reviewers can see whether a component uses approved sizes and tones.
When a page wants a one-off override, the team needs a variant, class convention, or wrapper.
Best for typed design-system contracts and static variant surfaces.
Good if React composition and override order become central.
Only compelling here if the system is already deeply MUI-based.
Flexible, but runtime styling is not the main goal for this stable system.
Launchpad Creator Feed
The same card appears in home feed, search results, promoted placements, compact sidebars, and partner embeds.
Each context wants small differences: tighter padding, hidden metadata, different emphasis, or local color accents.
This highlights StyleX because the important question is which caller is allowed to override which property.
stylex.props can place component defaults first and caller style last. The winner is visible at the element boundary.
Teams must accept StyleX static-analysis rules. Arbitrary object spreading and imported dynamic style values become migration pain.
Good for named variants, weaker when every caller needs local final say.
Best for deterministic property-level overrides across component boundaries.
sx can help MUI consumers, but extraction limits still shape the API.
Also strong for callsite overrides, but with runtime style generation.
Meridian Admin Suite
A mature MUI app has hundreds of Box sx props, styled components, theme overrides, and Next.js App Router adoption 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.
This highlights Pigment CSS because it is designed around MUI migration: @mui/material-pigment-css, transformLibraries, sx, styled, and theme config.
Developers can keep familiar MUI authoring patterns while moving supported styles into build output.
The current alpha/on-hold status, pnpm caveat, and runtime-value migration rules make it risky as a default platform bet.
Good static target, but not a MUI semantics migration path.
Technically strong, but would be a larger architecture migration.
Best functional fit for this specific migration, with explicit adoption risk.
Keeps today working, but does not solve the static/RSC pressure.
Signal Campaign Studio
Marketers drag blocks, choose brand colors, preview layouts, and save experiments while the page updates instantly.
Many values come from user input, CMS fields, or preview-only combinations that are not known at build time.
This highlights Emotion because runtime styling is not a weakness here; it is the reason the experience is easy to build.
css arrays and dynamic objects can directly reflect user input. The implementation stays close to live preview state.
If prototype code hardens into production, temporary dynamic styles can become the design system by accident.
Useful for the editor shell, but not ideal for arbitrary user-generated styling.
Can use variables, but arbitrary dynamic objects fight the compiler model.
Runtime values must be reworked into CSS variables or wrapper styles.
Best for highly dynamic preview surfaces and runtime style composition.
Atlas Launch Pages
A team ships docs pages, landing pages, pricing pages, and a small logged-in widget from one shared design language.
The public site wants static assets, predictable CSS, and low JavaScript. The widget still needs component-level consistency.
This highlights vanilla-extract because it treats styling as build output while keeping design tokens in TypeScript.
The marketing site gets static CSS and shared token contracts without adopting a React-specific styling mental model everywhere.
If the embedded widget later becomes override-heavy, StyleX may fit that widget better than the shared site styling system.
Best when cross-framework static output and typed tokens matter together.
Strong inside the React widget, less compelling for non-React surfaces.
Hard to justify unless the pages are already MUI-centered.
Convenient, but adds runtime styling where static output is the product goal.
LedgerFlow Back Office
An older internal app has dozens of components accepting className, style, and theme props from different product teams.
Nobody fully trusts CSS order. A small change in one shared component can change spacing or colors in a distant screen.
This highlights StyleX because deterministic composition directly answers "I passed an override, why did it not win?"
The migration can make override order explicit: component defaults, feature state, then caller style.
Legacy dynamic patterns may need cleanup before they compile. The migration may be architectural, not a search-and-replace.
Good for rebuilding stable components, less direct for override-order cleanup.
Best when override determinism is the migration goal.
Useful only if the legacy app is specifically MUI/Emotion-based.
Practical for incremental migration, but keeps runtime styling in place.
Checkout Experiment Room
A product team is testing checkout flows, layouts, and color treatments before committing to a design-system change.
The layout changes every week. Most style decisions are temporary, and developer speed matters more than long-term static output.
This highlights Emotion because its flexibility is valuable before the team knows which abstractions deserve to become permanent.
Developers can compose temporary states directly in the component and delete them later without designing a full variant API.
If prototype code becomes production code, runtime styling can become the accidental design system.
Useful once the experiment stabilizes, heavier while ideas are volatile.
Good if the prototype already follows final app constraints.
Too much migration risk unless the prototype is testing MUI migration itself.
Best for speed and exploratory composition before rules stabilize.
Comparison matrix
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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
These are intentionally small examples. They are not exhaustive production snippets; they are meant to make the composition model visible.
This is the design-system case: a small set of supported combinations should be easy to type, audit, and reuse.
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.
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.
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.
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.
This is the component-library case: consumers need a controlled way to adjust one component without fighting CSS order.
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.
function Card(props) {
return <section {...stylex.props(styles.card, props.style)} />
}
<Card style={styles.compact} />Excellent when the caller should decide final overrides.
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.
function Card({ css: cssProp }) {
return <section css={[card, cssProp]} />
}
<Card css={compact} />The caller override pattern is very direct, with runtime flexibility.
This is the runtime-value case: the value is not just conditional, it is unknown until a person or server sends it.
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.
const styles = stylex.create({
badge: (color) => ({ color }),
})
<span {...stylex.props(styles.badge(userColor))} />Dynamic functions make the variable bridge explicit.
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.
<span css={{ color: userColor }} />
// Direct and ergonomic because Emotion can generate CSS at runtime.Best ergonomics when arbitrary runtime values are the product requirement.
Most styling bugs are not about setting a value. They are about understanding which value wins when two values collide.
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.props(styles.base, styles.primary, styles.danger)
// danger wins because it is applied last.The winning rule is visible where the element is rendered.
<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.
<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
Build-time classlist composition
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.
const base = style({ padding: 12 })
const primary = style([base, { background: 'blue' }])
// primary is a reusable classlist string
// built before the browser runs.Callsite-first deterministic composition
Composition happens where the element is rendered. stylex.props receives styles, arrays, falsy conditionals, and style props, then merges by application order.
<div
{...stylex.props(
styles.base,
isHighlighted && styles.highlighted,
props.style,
)}
/>MUI-like composition under extraction rules
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.
const Heading = styled('h1')({
fontSize: '2rem',
variants: [
{ props: { tone: 'danger' }, style: { color: 'red' } },
],
})
<div sx={[baseSx, compactSx]} />Runtime composition and insertion-order control
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.
<div css={[base, danger]}>Red wins</div>
<div css={[base, cssPropFromCaller]} />Recommendation
You are building a typed design system, need explicit variant APIs, want framework portability, and prefer CSS-module-like build artifacts.
Your app is React-heavy, component reuse is deep, override order matters at the element boundary, and strict compiler rules are acceptable.
You are specifically evaluating a Material UI Emotion migration and can accept early-alpha, currently-on-hold project risk with a fallback path.
You need high dynamic styling flexibility, live preview ergonomics, or a prototype surface where runtime style generation is the feature, not the problem.
Evidence