Skip to main content

Thinking in StyleX

Core Principles

To understand why StyleX exists and the reasoning behind its decisions, it may be beneficial to familiarize oneself with the fundamental principles that guide it. This may help you decide if StyleX is the right solution for you.

These principles should also be helpful when designing new APIs for StyleX.

Co-location

There are benefits of DRY code, but we don't think that's usually true when it comes to authoring styles. The best and most readable way to write styles is to write them in the same file as the markup.

StyleX is designed for authoring, applying, and reasoning about styles locally.

Deterministic resolution

CSS is a powerful and expressive language. However, it can sometimes feel fragile. Some of this stems from a misunderstanding of how CSS works, but a lot of it stems from the discipline and organization required to keep CSS selectors with different specificities from conflicting.

Most existing solutions to this problem rely on rules and conventions.

BEM and OOCSS ConventionsBEM and OOCSS introduce naming conventions to avoid these problems relying on developers to consistently follow the rules, frequently avoiding merging styles at all. This can lead to bloated CSS.
Utility ClassesAtomic utility class names like Tailwind CSS and Tachyons rely on conventions and lint rules to ensure that conflicting class names are not applied on the same element. Such tooling adds constraints on where and how styles can be applied, putting architectural limitations on styling.

StyleX aims to improve both the consistency and predictability of styles and the expressive power available. We believe this is possible through build-tools.

StyleX provides a completely predictable and deterministic styling system that works across files. It produces deterministic results not only when merging multiple selectors, but also when merging multiple shorthand and longhand properties. (e.g. margin vs margin-top).

"The last style applied always wins."

Low-cost abstractions

When it comes to the performance cost of StyleX, our guiding principle is that StyleX should always be the fastest way to achieve a particular pattern. Common patterns should have no runtime cost and advanced patterns should be as fast as possible. We make the trade-off of doing more work at build-time to improve runtime performance.

Here's how this plays out in practice:

1. Styles created and applied locally

When authoring and consuming styles within the same file, the cost of StyleX is zero. This is because in addition to compiling away stylex.create calls, StyleX also compiles away stylex.props calls when possible.

So,

import * as stylex from 'stylex';
const styles = stylex.create({
red: {color: 'red'},
});
let a = stylex.props(styles.red);

Compiles down to:

import * as stylex from 'stylex';

let a = {className: 'x1e2nbdu'};

There is no runtime overhead here.

2. Using styles across files

Passing styles across file boundaries incurs a small cost for the additional power and expressivity. The stylex.create call is not deleted entirely and instead leaves behind an object mapping keys to class names. And the stylex.props() calls are executed at runtime.

This code, for example:

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

const styles = stylex.create({
foo: {
color: 'red',
},
bar: {
backgroundColor: 'blue',
},
});

function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}

Compiles down to:

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

const styles = {
foo: {
color: 'x1e2nbdu',
$$css: true,
},
bar: {
backgroundColor: 'x1t391ir',
$$css: true,
},
};

function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}

This is a little more code, but the runtime cost is still minimal because of how fast the stylex.props() function is.

Most other styling solutions don't enable composition of styles across file boundaries. The state of the art is to combine lists of class names.

Small API surface

Our goal is to make StyleX as minimal and easy-to-learn as possible. As such we don't want to invent too many APIs. Instead, we want to be able to lean on common JavaScript patterns where possible and provide the smallest API surface possible.

At its core, StyleX can be boiled down to two functions:

  1. stylex.create
  2. stylex.props

stylex.create is used to create styles and stylex.props is used to apply those styles to an element.

Within these two functions, we choose to rely on common JS patterns rather than introduce unique APIs or patterns for StyleX. For example, we don't have an API for conditional styles. Instead, we support applying styles conditionally with boolean or ternary expressions.

Things should work as expected when dealing with JavaScript objects and arrays. There should be no surprises.

Type-Safe styles

TypeScript has become massively popular due to the experience and safety it provides. Our styles, however, have largely remained untyped and unreliable. Other than some path-breaking projects such as Vanilla Extract, styles are just bags of strings in most styling solutions.

StyleX is authored in Flow with strong static types. Its packages on NPM come with auto-generated types for both Flow and TypeScript. When there are incompatibilities between the two type-systems, we take the time to ensure that we write custom TypeScript types to achieve the same level of power and safety as the original Flow.

All styles are typed. When accepting styles as props, types can be used to constrain what styles are accepted. Styles should be as type-safe as any other component props.

The StyleX API is strongly typed. The styles defined with StyleX are typed too. This is made possible by using JavaScript objects to author raw styles. This is one of the big reasons we have chosen objects over template strings.

These types can then be leveraged to set contracts for the styles that a component will accept. For example, a component props can be defined to only accept color and backgroundColor but no other styles.

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

type Props = {
//...
style?: StyleXStyles<{color?: string; backgroundColor?: string}>;
//...
};

In another example, the props may disallow margins while allowing all other styles.

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

type Props = {
//...
style?: StyleXStylesWithout<{
margin: unknown;
marginBlock: unknown;
marginInline: unknown;
marginTop: unknown;
marginBottom: unknown;
marginLeft: unknown;
marginRight: unknown;
marginBlockStart: unknown;
marginBlockEnd: unknown;
marginInlineStart: unknown;
marginInlineEnd: unknown;
}>;
//...
};

Styles being typed enables extremely sophisticated rules about how a component's styles can be customized with zero-runtime cost.

Shareable constants

CSS class names, CSS variables, and other CSS identifiers are defined in a global namespace. Bringing CSS strings into JavaScript can mean losing type-safety and composability.

We want styles to be type-safe, so we've spent a lot of time coming up with APIs to replace these strings with references to JavaScript constants. So far this is reflected in the following APIs:

  1. stylex.create Abstracts away the generated class names entirely. You deal with "opaque" JavaScript objects with strong types to indicate the styles they represent.
  2. stylex.defineVars Abstracts away the names of CSS variables generated. They can be imported as constants and used within styles directly.
  3. stylex.keyframes Abstracts away the names of keyframe animations. Instead they are declared as constants and used by reference.

We're looking into ways to make other CSS identifiers such as container-name and @font-face type-safe as well.

Framework-agnostic

StyleX is a CSS-in-JS solution, not a CSS-in-React solution. Although StyleX has been tailored to work best with React today, it is designed to be used with any JavaScript framework that allows authoring markup in JavaScript. This includes frameworks that use JSX, template strings, etc.

stylex.props returns an object with className and style properties. A wrapper function may be needed to convert this to make it work with various frameworks.

Encapsulation

All styles on an element should be caused by class names on that element itself.

CSS makes it very easy to author styles in a way that can cause "styles at a distance":

  • .className > *
  • .className ~ *
  • .className:hover > div:first-child

All of these patterns, while powerful, make styles fragile and less predictable. Applying class names on one element can affect a completely different element.

Inheritable styles such as color will still be inherited, but that is the only form of style-at-a-distance that StyleX allows. In those cases too, the styles applied directly on an element always take precedence over inherited styles.

This is often not the case when using complex selectors, as the complex selectors usually have higher specificity than the simple class selectors used for styles applied directly on the element.

StyleX disallows this entire class of selectors. This currently makes certain CSS patterns impossible to achieve with StyleX. Our goal is to support these patterns without sacrificing style encapsulation.

StyleX is not a CSS pre-processor. It intentionally puts constraints on the power of CSS selectors in order to build a fast and predictable system. The API, based on JavaScript objects instead of template strings, is designed to make these constraints feel natural.

Readability & maintainability over terseness

Some recent utility-based styling solutions are extremely terse and easy to write. StyleX chooses to prioritize readability and maintainability over terseness.

StyleX makes the choice to use familiar CSS property names to prioritize readability and a shallow learning curve. (We did decide to use camelCase instead of kebab-case for convenience.)

We also enforce that styles are authored in objects separate from the HTML elements where they are used. We made this decision to help with the readability of HTML markup and for appropriately named styles to indicate their purpose. For example, using a name like styles.active emphasizes why styles are being applied without having to dig through what styles are being applied.

This principle leads to trade-offs where authoring styles may take more typing with StyleX than some other solutions.

We believe these costs are worth the improved readability over time. Giving each HTML element a semantic name can communicate a lot more than the styles themselves.

info

One side benefit of using references to styles rather than using the styles inline is testability. In a unit-testing environment, StyleX can be configured to remove all atomic styles and only output single debugging class names to indicate the source location of styles rather than the actual styles.

Among other benefits, it makes snapshot tests more resilient as they won't change for every style change.

Modularity and composability

NPM has made it extremely easy to share code across projects. However, sharing CSS has remained a challenge. Third-party components either have styles baked in that are hard or impossible to customize, or are completely unstyled.

The lack of a good system to predictably merge and compose styles across packages has also been an obstacle when sharing styles within packages.

StyleX aims to create a system to easily and reliably share styles along with components within packages on NPM.

Avoid global configuration

StyleX should work similarly across projects. Creating project-specific configurations that change the syntax or behavior of StyleX should be avoided. We have chosen to prioritize composability and consistency over short-term convenience. We lean on linting and types to create project-specific rules.

We also avoid magic strings that have special meaning within a project globally. Instead, every style, every variable, and every shared constant is a JavaScript import without needing unique names or project configuration.

One small file over many smaller files

When dealing with a large amount of CSS, lazy-loading CSS is a way to speed up the initial load time of a page. However, it comes at the cost of slower update times, or the Interaction to Next Paint (INP) metric. Lazy-loading any CSS on a page triggers a recalculation of styles for the entire page.

StyleX is optimized for generating a single, highly optimized, CSS bundle that is loaded upfront. Our goal is to create a system where the total amount of CSS is small enough that all the CSS can be loaded upfront without a noticeable performance impact.

Other techniques to make the initial load times faster, such as "critical CSS" are compatible with StyleX, but should normally be unnecessary.