Framework comparison
How TypeStyles compares to StyleX, Panda CSS, vanilla-extract, Emotion, CSS Modules, and plain CSS—with a shared button example and a topic-by-topic comparison
This page is a decision lens: same button pattern across ecosystems, then a topic-by-topic comparison (easier to scan on a laptop than a wide grid of cards or a seven-column table). For API-by-API moves, use the Migration guide. For install and your first component, see Getting started.
Compared at a glance
Each subsection below is one decision axis. Lists use normal body typography—tool names and inline code carry the emphasis.
Build
- TypeStyles — Optional; runtime by default, zero-runtime when you want.
- StyleX — Required compiler (Babel / SWC).
- Panda CSS — Required codegen + config.
- vanilla-extract — Required
.css.ts+ bundler. - Emotion / styled-components — Bundler only; runtime in the loop.
- CSS Modules — Bundler maps your authored class names to per-file scoped output.
- Plain CSS — None for CSS itself.
Typical DOM classes
- TypeStyles — Readable names (
button-intent-primary); scoped--scope-token-*. - StyleX — Atomic, hashed.
- Panda CSS — Utilities / recipes from your preset.
- vanilla-extract — Usually hashed / scoped.
- Emotion / styled-components — Hashed.
- CSS Modules — You choose readable names in CSS; the bundler emits scoped class strings in the DOM, usually with a short content hash so files cannot collide (for example
._primary_abc123). - Plain CSS — Whatever you author.
Tokens
- TypeStyles —
tokens.create→ real CSS variables. - StyleX —
defineVars+ compiler. - Panda CSS — Config-first scales (
blue.600, etc.). - vanilla-extract — You wire CSS vars / contracts.
- Emotion / styled-components — Theme objects / your own vars.
- CSS Modules — Your globals / vars.
- Plain CSS — Your conventions.
Good fit when…
- TypeStyles — TS variants and inspectable CSS without a compiler on day one.
- StyleX — Meta stack, static guarantees, atomic output.
- Panda CSS — You want Panda’s pipeline and strict token schema.
- vanilla-extract — Zero runtime by default, file-based CSS.
- Emotion / styled-components — Classic styled API; fine with runtime (or separate extraction).
- CSS Modules — Mostly hand-written CSS; variants via
clsx/ toggles. - Plain CSS — Maximum portability; TS ergonomics are DIY.
SSR / production CSS: TypeStyles can collect styles for SSR and optionally extract static CSS. StyleX, Panda, and vanilla-extract generally emit CSS at build time. Emotion’s story depends on your bundler and extraction setup. CSS Modules and plain CSS ship as stylesheets like any static asset.
Same button: primary vs ghost
Each snippet is a typed intent (primary = filled, ghost = outline). Real apps add focus, sizing, and tokens; lengths are trimmed.
TypeStyles (recommended shape)
createTypeStyles keeps styles and tokens on one scopeId (see Getting started).
import { createTypeStyles } from 'typestyles';
const { styles, tokens } = createTypeStyles({ scopeId: 'app' });
const color = tokens.create('color', { primary: '#2563eb', surface: '#ffffff' });
export const button = styles.component('button', {
base: {
padding: '8px 16px',
borderRadius: '6px',
fontWeight: 500,
border: 'none',
cursor: 'pointer',
},
variants: {
intent: {
primary: { backgroundColor: color.primary, color: color.surface },
ghost: {
backgroundColor: 'transparent',
color: color.primary,
border: `1px solid ${color.primary}`,
},
},
},
defaultVariants: { intent: 'primary' },
});
<button type="button" className={button({ intent })}>
{children}
</button>
StyleX (compiler, atomic classes)
Author-time objects; stylex.props merges styles at compile time. Hashed atomic classes in the DOM.
import * as stylex from '@stylexjs/stylex';
const s = stylex.create({
base: {
padding: '8px 16px',
borderRadius: '6px',
fontWeight: 500,
border: 'none',
cursor: 'pointer',
},
primary: { backgroundColor: '#2563eb', color: '#fff' },
ghost: {
backgroundColor: 'transparent',
color: '#2563eb',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: '#2563eb',
},
});
<button type="button" {...stylex.props(s.base, intent === 'ghost' ? s.ghost : s.primary)}>
{children}
</button>
Panda CSS (cva / recipes)
Codegen from panda.config; import paths vary (../styled-system/css here is illustrative). Token strings like blue.600 are your scale.
import { cva } from '../styled-system/css';
export const button = cva({
base: { px: '4', py: '2', rounded: 'md', fontWeight: 'medium', cursor: 'pointer' },
variants: {
intent: {
primary: { bg: 'blue.600', color: 'white', borderWidth: '0' },
ghost: { bg: 'transparent', color: 'blue.600', borderWidth: '1px', borderColor: 'blue.600' },
},
},
defaultVariants: { intent: 'primary' },
});
<button type="button" className={button({ intent })}>
{children}
</button>
vanilla-extract (recipe)
.css.ts files; bundler emits static CSS. Typed variants; class strings usually hashed.
import { recipe } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
padding: '8px 16px',
borderRadius: '6px',
fontWeight: 500,
border: 'none',
cursor: 'pointer',
},
variants: {
intent: {
primary: { background: '#2563eb', color: '#fff' },
ghost: { background: 'transparent', color: '#2563eb', border: '1px solid #2563eb' },
},
},
defaultVariants: { intent: 'primary' },
});
<button type="button" className={button({ intent })}>
{children}
</button>
Emotion / styled-components
Styled component or css prop; hashed classes; tokens are usually theme objects or vars you wire.
import styled from '@emotion/styled';
export const Button = styled.button<{ intent?: 'primary' | 'ghost' }>`
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: none;
${(p) =>
p.intent === 'ghost'
? `background: transparent; color: #2563eb; border: 1px solid #2563eb;`
: `background: #2563eb; color: #fff;`}
`;
<Button intent={intent}>{children}</Button>
CSS Modules
Plain CSS in .module.css; bundler scopes names. Variants are manual (clsx, toggles, BEM).
/* Button.module.css */
.base {
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: none;
}
.primary {
background: #2563eb;
color: #fff;
}
.ghost {
background: transparent;
color: #2563eb;
border: 1px solid #2563eb;
}
import styles from './Button.module.css';
import clsx from 'clsx';
<button
type="button"
className={clsx(styles.base, intent === 'ghost' ? styles.ghost : styles.primary)}
>
{children}
</button>;
Plain CSS
Global stylesheets, BEM modifiers, or attribute selectors—no TS variant layer unless you add one yourself. Maximum portability; colocation and typing are DIY.
Theming architecture: TypeStyles vs. StyleX (and Astryx)
Theming is where compiler restrictions surface in practice. Astryx—Meta's design system for internal tools, built on StyleX—has a genuinely flexible theming story, but it gets there by working around StyleX's constraints. TypeStyles never had those constraints, so the workaround layer doesn't exist. Three concrete differences:
Token variables: plain custom properties vs. compiler-managed vars
- TypeStyles —
tokens.createemits plain CSS custom properties with predictable names (--app-color-primaryunderscopeId: 'app') from any ordinary TypeScript module. Themes aretokens.createThemesurfaces: a stabletheme-{name}class whose properties override token values for that subtree. Because the variables are just CSS, any stylesheet—yours, legacy, third-party—can read or set them directly. - StyleX —
defineVarsalso compiles to custom properties, but with hashed names the compiler owns, and variable definitions must live in.stylex.ts/.stylex.jsfiles so imports can be resolved statically; theming (createTheme) flows through those same files. Astryx's theming flexibility is built by routing around these rules. With TypeStyles there is no compiler restriction to route around.
TypeStyles theme surfaces also carry a general condition engine—tokens.when (media queries, attributes, class names, or / and / not combinators) plus tokens.colorMode presets—so "dark when the OS prefers it, unless data-color-mode="light" wins" is a declaration, not bespoke infrastructure on top of fixed light/dark modes.
Distribution: build-always vs. runtime injection with a warning
- TypeStyles — Token and theme CSS goes through the same pipeline as every other style: runtime injection in dev, static CSS via zero-runtime extraction when you want it. There is no dedicated theme-build command, because none is needed—and therefore no "did you forget to build your theme" state to warn about.
- Astryx — Themes that weren't compiled ahead of time are injected at runtime with a console warning, and users are pushed toward a separate
astryx theme buildstep to get static CSS.
Component overrides: plain CSS vs. a config DSL
- TypeStyles —
styles.componentemits semantic, deterministic class names (button-intent-primary). A consumer restyling a component writes ordinary CSS targeting that class—any property, any selector, any stylesheet—and cascade layers keep override order predictable. - StyleX / Astryx — Hashed atomic classes can't be targeted from outside the compiler, so Astryx exposes overrides through an
@scope+ data-attribute configuration DSL: you can override what the DSL anticipates, in the shapes it anticipates.
The pattern across all three: a StyleX-based system must generate an escape hatch for each theming capability its compiler forecloses. TypeStyles ships the underlying primitives—real custom properties, readable class names, cascade layers—so the capability is the default, not the workaround.
When TypeStyles is a strong default
- Readable classes and scoped CSS variables for DevTools, legacy CSS, and third-party markup.
- Typed variants (CVA-/recipe-like) without a compiler on day one.
- Incremental adoption and
createTypeStyles+scopeIdfor libraries or micro-frontends. - Comfortable with runtime injection in dev, with an optional zero-runtime path when you need static CSS.
When another tool might win
- StyleX — Standardized on Meta’s compiler; want static guarantees and atomic output.
- Panda CSS — Want codegen utilities and a strict config-first token pipeline.
- vanilla-extract — Want zero runtime by default and are fine with
.css.tscontracts. - Emotion / styled-components — Want the classic styled API and accept runtime (or a separate extraction story).
- CSS Modules — Mostly hand-written CSS; no first-class variant API in the styling layer.
- Plain CSS — Zero JS styling layer; maximum portability.
Practical migration
Start with Migration: Panda- and CVA-like APIs map closely to styles.component; Emotion and CSS Modules map well to styles.class plus cx from 'typestyles'.