Ga naar hoofdinhoud

StyleX, just another styling library?

by Carl — Sep 08, 2026
8 minutes

When I first heard about StyleX, I dismissed it as yet another CSS-in-JS library promising to fix styling. We have seen plenty of those, and most of them dissolve into the same naming conventions and runtime costs we already had. But you cannot judge what you do not know, so I decided to look into it properly.

Once I understood the reasoning behind the system, I got enthusiastic. StyleX is not another attempt at the same thing. It is a set of deliberate constraints that make CSS behave predictably at any scale, and those constraints are worth understanding even if you never adopt the library. The ideas behind it feel more relevant than ever now that we use agents to write more of our code every day.

What StyleX actually does

You author styles as plain TypeScript objects next to your component. A build step then extracts every declaration into a static CSS file. Your production app ships a stylesheet and class names on elements. No runtime style injection, no compiler work in the browser.

Here is the core of it in TypeScript:

import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
  card: {
    backgroundColor: "#ffffff",
    borderRadius: "12px",
    boxShadow: "0 1px 4px rgba(0, 0, 0, 0.08)",
    padding: "16px",
  },
});

const classNames = stylex.props(styles.card).className;
// Apply `classNames` to your element however your framework allows
typescript

stylex.create() defines the rules, stylex.props() merges them and produces the class names. During the build, the compiler turns each declaration into static CSS. The object you wrote disappears from the shipped code. What remains is a stylesheet plus short generated class names.

Principle 1: co-location

Most styling pain comes from distance. The markup lives in one file, the styles in another, and connecting them requires searching, memorizing, and guessing. StyleX drops that distance. You write styles in the same file as the component that uses them.

This sounds trivial, but it goes against how we usually think about clean code. DRY says: remove duplication, centralize things. For styles, Meta argues the opposite. Duplication across files is cheaper than indirection, because the readable way to write styles is next to the markup that renders them.

The benefit compounds when someone else reads your code, including an agent. A component file shows everything that matters: structure, behavior, and appearance. No jumping between files, no searching a global stylesheet for a class that appears in five places.

Deterministic resolution

Traditional CSS resolves conflicts through the cascade. Source order, specificity, and inheritance interact, and the browser figures it out per element. That flexibility is also the source of the bugs. Two rules fight, one wins for the wrong reason, and you fix it with a higher specificity, which breaks the next conflict.

StyleX removes the guesswork with two fixed rules:

  1. When two styles set the same property, the last style applied through stylex.props() wins.
  2. When styles overlap through shorthand and longhand, the more specific property wins by default, so marginTop beats margin regardless of the order in which you applied the styles.
import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
  base: {
    color: "black",
    margin: 0,
  },
  featured: {
    color: "rebeccapurple",
    marginTop: "12px",
  },
});

// Later style wins for `color`.
// `marginTop` beats `margin` because it is the more specific property,
// regardless of the order of the arguments.
const classNames = stylex
  .props(styles.base, styles.featured)
  .className;
typescript

You can change the default behavior through the styleResolution compiler option, but you configure it once, globally, not per conflict. Either way the resolution is deterministic. The same inputs produce the same output, in every file, every time. There is no cascade to debug.

Type safety as the product

Why JavaScript objects and not template strings that look like real CSS? Because objects let TypeScript check every declaration. A misspelled property or an invalid value fails during development, not in production.

The typing also works as a contract between components. A component can declare exactly which styles callers may pass:

import type { StyleXStyles } from "@stylexjs/stylex";

type BadgeProps = {
  // Callers may only override color and backgroundColor.
  style?: StyleXStyles<{
    color?: string;
    backgroundColor?: string;
  }>;
};
typescript

Or block styles entirely:

import type { StyleXStylesWithout } from "@stylexjs/stylex";

type CardProps = {
  // Callers may customize anything except margins.
  style?: StyleXStylesWithout<{
    margin: unknown;
    marginTop: unknown;
    marginBottom: unknown;
    marginLeft: unknown;
    marginRight: unknown;
  }>;
};
typescript

These contracts run at compile time, so they cost nothing at runtime. In exchange, your editor autocompletes style objects down to every property and value. You cannot type a property that does not exist, and neither can an agent.

What's in it for you?

This is the payoff section. Co-location, deterministic resolution, and type safety are the inputs. This output is the reason all three exist: less CSS shipped, less CSS parsed, no duplication in the result.

StyleX emits one class for each unique property-value pair. Author a rule with ten declarations, and you get ten atomic classes, not one bundled class. If a declaration appears in a thousand places across the app, the compiler reuses the same single class each time.

This produces smaller stylesheets on purpose. Gzip already compresses repeated strings well, so the byte savings on the wire are modest. The parsing cost is real, though. Every rule the browser downloads, it parses and stores, even the duplicates. Fewer, shorter rules means less of that work.

The principles and this outcome are one system, not separate features. Co-location keeps each rule local, so the compiler sees every declaration and can deduplicate across the whole codebase. Deterministic resolution makes it safe to merge styles into atomic classes, because the outcome never depends on source order or specificity. Type safety guarantees the compiler sees a closed set of declarations rather than arbitrary strings. Remove any one of those inputs and the payoff shrinks with it.

Colocation and atomics also combine nicely for reuse. Since styles are imported TypeScript constants, sharing a cardBase definition between components is a plain import, not a copy-paste of utility strings or a wrapper component.

This section also sets up the next one: a smaller, deduplicated stylesheet with typed contracts is exactly the output an agent cannot quietly degrade, which is why the constraints matter even more when agents do the writing.

Why constraints matter for agents

Most of these properties existed before AI, and they were nice to have. Agents change that. A model generating plain CSS tends to invent class names that do not exist, duplicate rules that already exist, and paper over specificity conflicts until the stylesheet becomes untouchable.

StyleX turns those failure modes into build errors:

import type { StyleXStyles } from "@stylexjs/stylex";
import * as stylex from "@stylexjs/stylex";

type ButtonProps = {
  style?: StyleXStyles;
};

const baseStyles = stylex.create({
  base: {
    backgroundColor: "#6d28d9",
    borderRadius: "8px",
    padding: "10px 20px",
  },
});

// Passing an untyped object or an undefined class
// fails the type check, not the runtime.
export function buttonClass(extra: StyleXStyles) {
  return stylex.props(baseStyles.base, extra).className;
}
typescript

A hallucinated constant fails the type check. An invalid property fails it too. Overrides happen in JavaScript, where merging objects is a solved problem, instead of through string surgery on class lists. The constraints that feel restrictive for a human writer are exactly what keeps an agent inside the design system.

Dynamic values

Static extraction raises the obvious question: what about values the build cannot know, like a width based on runtime data? StyleX handles this with CSS variables. The compiler emits a static rule that references a variable, and the runtime value lands in inline styles:

import * as stylex from "@stylexjs/stylex";

const [vars] = stylex.defineVars({
  barWidth: "0%",
});

const styles = stylex.create({
  bar: {
    width: vars.barWidth,
    height: "8px",
    backgroundColor: "#e5e7eb",
  },
});

interface ProgressBarProps {
  percent: number;
}

export function ProgressBar({ percent }: ProgressBarProps) {
  return (
    <div
      {...stylex.props(styles.bar)}
      style={{ "--barWidth": `${percent}%` } as React.CSSProperties}
    />
  );
}
typescript

The stylesheet stays static, the dynamic part stays inline, and both arrive in the same class output. StyleX offers matching APIs for defineVars, themes, and keyframes, so the pattern extends to design tokens and animations.

The honest downsides

StyleX trades ergonomics for predictability, and the trade is visible. Writing styles by hand in nested objects is worse than writing CSS. Pseudo-classes, media queries, and newer selector-based features get verbose fast. Nesting selectors inside each other, which native CSS supports and most of us like, mostly disappears, because every child element gets its own style object.

Some cutting-edge CSS does not map cleanly to typed objects either. The pragmatic answer is to step out to a plain stylesheet for those cases, which StyleX permits. The risk is that a codebase drifts into two systems side by side, so the escape hatch needs rules attached to it.

If the object syntax is the dealbreaker, Panda CSS is the nearest alternative. It offers the same build-time model with both object syntax and tagged-template syntax, so you can author something closer to real CSS while keeping the compiled output.

When to adopt it

StyleX earns its constraints when styling stops being a technical problem and becomes an organizational one. The signals are familiar: specificity debugging sessions, discussions about naming conventions, repeated override hacks, and hesitation to change a rule that ships in the same file as twenty components. That is the situation StyleX was designed for, and it is a better adoption argument than novelty.

A healthy project does not need a migration just to try a new styling library. A greenfield project, or a codebase where CSS has turned into coordination overhead, is where the principles pay off.

Summary

StyleX trades a pleasant authoring experience for a system that behaves the same way at 20 pages and 200,000 components. Its principles carry the value: styles sit next to markup, conflicts resolve deterministically, everything is checked by the type system at no runtime cost, and the atomic output keeps the shipped CSS small. Those same constraints are what make it attractive when agents write most of the styling.

Next steps: read the "Thinking in StyleX" page in the official docs at stylexjs.com/docs, then try the playground on the site to feel the typed autocomplete yourself. The core concepts don't depend on React.

If you're on Angular, StyleX works but requires setup. You'll need to configure Babel in your build pipeline, whether you're using Webpack or esbuild. The Angular CLI doesn't ship with official StyleX support like Next.js does, so expect to spend time on bundler configuration. Check the StyleX GitHub discussions for Angular integration examples and community solutions. Some teams have published unofficial webpack setups that might save you work.

Have Fun!