Skip to main content

StyleX: Giving Design Systems Stronger Types

September 1, 20265 min read

A shared button has a size option. Then someone adds custom padding where they use it. The component still renders, but its size no longer means the same thing everywhere.

A tweet about StyleX caught my attention because it focused on this problem. The feature that interests me is the ability to type the styles a component accepts.

A component can describe which parts of its appearance callers can change.

That gives a design system more control over how people use its components. It also makes those decisions visible in code.

Make customization part of the component contract

A design system combines reusable components, shared style values, and rules for using them. Those rules include decisions about who controls spacing, colors, and sizes.

Consider a button that lets the page control its top margin and make it full width. The button keeps control of its padding and shape. That agreement is its styling contract.

A general CSS type checks whether a property and value make sense as CSS. Our contract asks a more specific question: does this component permit that change?

StyleX uses typed JavaScript objects to define styles. Those types let components describe the styles they accept.

Here is a small example for a project that already has StyleX configured:

import * as stylex from '@stylexjs/stylex';
import type { ReactNode } from 'react';
 
type ButtonProps = {
  children: ReactNode;
  style?: stylex.StyleXStyles<{
    marginTop?: 0 | 8 | 16;
    width?: '100%';
  }>;
};
 
const buttonStyles = stylex.create({
  base: {
    paddingBlock: 12,
    paddingInline: 20,
    borderRadius: 8,
  },
});
 
export function Button({ children, style }: ButtonProps) {
  return (
    <button type="button" {...stylex.props(buttonStyles.base, style)}>
      {children}
    </button>
  );
}

This example focuses on styling. A production button also needs the behavior and accessibility features its use requires.

The style prop accepts StyleX styles with the listed properties and values. It permits three top margins and one width value. The StyleXStyles reference describes this type.

The component defines its own padding and border radius. Callers use the narrower contract. They do not receive permission to change every property the component uses internally.

See the rule where you use the button

Now define some styles in a separate page:

import * as stylex from '@stylexjs/stylex';
import { Button } from './Button';
 
const pageStyles = stylex.create({
  placement: {
    marginTop: 16,
    width: '100%',
  },
  customPadding: {
    paddingInline: 28,
  },
  customMargin: {
    marginTop: 12,
  },
});
 
export function Example() {
  return (
    <>
      <Button style={pageStyles.placement}>Save</Button>
      {/* Type error: the contract does not permit paddingInline. */}
      <Button style={pageStyles.customPadding}>Save</Button>
      {/* Type error: the contract does not permit marginTop: 12. */}
      <Button style={pageStyles.customMargin}>Save</Button>
    </>
  );
}

The first button follows the contract. TypeScript reports errors for the other two when you check this example. Those styles contain valid CSS, but they do not meet this button's rules.

The static types guide covers limits on properties and their values. It also describes StyleXStylesWithout, which excludes listed properties while permitting others.

For this button, I prefer listing the permitted properties. That keeps the intended customization small and easy to review.

The types travel with the styles

The page defines styles through stylex.create. The button receives those style objects and applies them through stylex.props. The create reference explains how StyleX produces compiled style objects.

The style prop here is a component input for StyleX objects. It is different from passing a plain object to a native element's React style prop.

The component combines its base styles with the caller's styles. For the same property, the later StyleX style takes priority. The props reference explains that merge behavior.

The type check controls the input before the application runs. It does not add a browser check that rejects forbidden styles.

Why this makes a design system stronger

I like how this forces a useful design decision. Which changes belong to the page, and which belong to the component?

That question can otherwise stay unanswered until someone changes the wrong thing. Writing the contract makes the team decide earlier.

It also gives component users a clearer starting point. They can inspect the accepted properties and values directly in the type. They have less reason to copy styling decisions from another page and hope those decisions apply.

Named options still have a place. A size="large" option can coordinate padding, text size, and height within the component. A restricted style prop can handle the few changes that callers need to control independently.

That balance matters to me. A reusable component needs enough flexibility to serve different pages, while keeping its design decisions consistent.

Keep the limits in view

StyleX documents TypeScript cases where extra properties can escape these checks. Its type reference also notes limits for unknown property names.

The types help during development. They do not prove that the rendered page looks right or that every route to changing styles follows the contract.

For example, exposing an unrestricted className alongside this prop gives callers another way to change the button. The component's other inputs need to support the same design decisions.

The team also needs to maintain the contract. If legitimate uses repeatedly need an exception, the component may need a new option. Strict rules only help when they describe a useful design system.

Give design decisions a place in code

This is what makes the idea interesting to me. A shared component carries both its default appearance and a clear description of permitted changes.

That makes a design system more useful when people build with it. The team can check part of its design intent before the code reaches review.

Update, September 15: I explored the same idea through lint rules in shadcn/lint: Design Rules Agents Can Check. That post looks at how errors can guide an agent toward an approved correction.

Which styling decisions should your components keep? Share your approach with me on X.