Skip to main content

· 8 min read
Naman Goel

Style Composition has never been easy

Style composition has never been easy with CSS. Applying multiple CSS rules to the same HTML element usually leads to a constant battle with CSS order, selector specificity, and growing CSS files.

CSS has evolved to provide additional tools such as @layer which makes the problem slightly better. However, the problem persists and the responsibility for solving the problem remains on tools such as StyleX.

Style Composition is desirable

Despite massive challenges with style composition, developers have always wanted ways to compose styles. Even before CSS-in-JS libraries became popular, developers used packages such as classnames to try and apply multiple sets of styles to the same element, despite all of the challenges that come with it.

In any design system, it is common to modify or override certain styles of components within certain contexts. It is also useful to be able to use component libraries and customize them to fit the design of the application. The alternative leads to unnecessary duplication of styles and code.

The rise of runtime CSS-in-JS libraries was fuelled, in part, by the need to solve the style composition problem. By being able to generate and inject styles at runtime, these libraries were able to provide a way to compose styles in a way that was not possible with traditional CSS. However, this also came with performance trade-offs.

In recent years, just as we've seen a rise in the popularity of atomic CSS, we've also seen utilities that merge such styles gain prevalence. Many tools now offer a way to merge styles.

Despite all the tools, style composition remains a challenge. From headless UI libraries to design system components that are copied to your own codebase, we believe many of these tools need to exist entirely because of the inconsistencies of merging styles.

We believe that we've built the first solution with style composition at its very core.

Inline Styles have always been composable

Despite the challenges of composing styles with CSS, inline styles have always been composable. In HTML, style accepts a string with a list of styles. In this string, the last style applied always wins.

Inline styles have their own limitations, too. They don't support pseudo classes, media queries, or other selectors. It is also difficult to enforce consistency when using inline styles directly in HTML.

However, component-based frameworks, such as React, largely sidestep any architectural issues with inline styles. Components are a layer of abstraction that enables code reuse without needing to write the same styles over and over again. This change was noticed early and was described in the original CSS-in-JS talk by Christopher Cheadeu.

So, when it came to designing StyleX, we decided to model our styles after inline styles. To form a mental model, it can be helpful to think of StyleX as "inline styles without the limitations".

StyleX gives you the ability to use capabilities such as pseudo classes and media queries, which are not possible with inline styles, all while maintaining the ability to compose styles in the same way that inline styles do.

Do "inline styles" need to be defined inline?

By the time we made the conscious decision to model StyleX after inline styles, StyleX had already been in development for a while and had evolved organically inspired by React Native's StyleSheet API, which was itself inspired by inline styles.

And so one of the first design decisions we reconsidered was the requirement to declare stylex.create before using it, without allowing the definition of styles inline. There are trade-offs to both approaches, we realised, that we had to allow the ability to statically define styles as JavaScript constants and be able to reuse them across multiple elements, multiple components, and even multiple files. Once we had this realization, we felt more comfortable not offering the ability to define styles inline. Even if occasionally inconvenient, it is better to have one consistent way to define styles.

The requirement to define styles at the top-level of a module also enforces some of the requirements of the compiler without feeling too unnatural.

Pseudo Classes and Media Queries

Our next design question was to decide how to handle pseudo classes, media queries and other at-rules in a way that felt like inline styles and enabled composability. Inline styles don't support pseudo classes or media queries, but it's possible to use JavaScript to read such 'conditions' and apply styles accordingly.

<div style={{
color: isActive ? 'white'
: isHovered ? 'blue'
: 'black',
width: window.matchMedia('(min-width: 1200px)').matches ? '25%'
: window.matchMedia('(min-width: 600px)').matches ? '50%'
: '100%'
}} />

This approach has obvious performance implications, but it also has some strong architectural benefits. The style object remains flat, making composition of styles more predictable. We avoid having to deal with complex rules for merging styles and dealing with specificity issues. We don't have to think about the 'default color' or the 'hover color'. We just think about a single 'color' value that changes based on the conditions.

All values for a property being co-located can also lead to a more consistent design system. Instead of thinking about mobile styles or desktop styles, this approach forces you to think responsively about the value of each property.

This realization led to one of our most unique design decisions. Instead of separating "base" styles and styles under a media query, which is common in almost every other CSS library, we decided to treat pseudo-classes and media queries as 'conditions' within the value of a property.

const styles = stylex.create({
color: {
default: 'black',
':hover': 'blue',
':active': 'white',
},
width: {
default: '100%',
'@media(min-width: 600px)': '50%',
'@media(min-width: 1200px)': '25%',
}
});

We found a way to take the concept of JavaScript conditions from inline styles and express them declaratively and statically in a way that can be compiled to static CSS while keeping the architectural benefits.

CSS Shorthands

CSS shorthand properties introduce another factor of confusion when composing styles. This decision has remained controversial even within the team itself, and so we still support two different strategies for merging shorthands.

React Native's Strategy

React Native pioneered the approach of merging purely by key, and assigning different priorities to different properties. Longhand properties have a higher priority than shorthand properties. So, while you can end up with a style object that has both padding and paddingTop, paddingTop will take precedence regardless of the order in which they are applied.

Inline Styles Strategy

Inline styles, on the other hand, always merge by order of application, and the last style applied always wins. Modelling this behaviour in library that compiles to static CSS wasn't obvious at first. We started by expanding shorthands into longhand properties at compile time. This approach had many edge cases and couldn't be done reliably in all cases.

We eventually discovered a strategy where a shorthand property would also set its constituent longhand properties to null. This would, in practice, "unset" any longhand that may have been applied before. This achieves the exact the same behaviour as inline styles, where the last style applied always wins.

This approach has some performance trade-offs. It can result in larger JavaScript objects after compilation. However, as our overall design philosophy was to model StyleX after inline styles, we decided to make this the default behaviour.

Dynamic Styles

Any API modelled after inline styles must support dynamic styles, but we knew we needed to do it with care and intention. We wanted to make it possible to use dynamic styles when needed, but we also wanted to make it explicit when styles were dynamic. We don't want an API that makes it easy to accidentally create dynamic styles.

StyleX allows dynamic styles by using functions. Instead of mixing inline objects with static styles created with stylex.create, functions let us define all kinds of styles in a single consistent way.

Behind the scenes, all styles, even dynamic ones, are compiled to static CSS. We don't apply any style property as an inline style. Only CSS variables are ever applied as inline styles and are used as the vehicle to let static styles have dynamic values. By not having any style properties as inline styles, we avoid any conflicts that may arise from inline styles having higher specificity than static styles. (It also makes certain compile-time optimizations possible, but that's its own story.)

Conclusion

CSS has been around for a long time now. It has evolved in many ways and is now both flexible and one of the most powerful layout models that exist. Yet, it remains a tool with sharp edges and it can be challenging to wield effectively.

We've seen many tools and libraries that have tried to make CSS easier to work with. Over the years, many problems have been solved, but the problem of style composition has persisted to some extent. Even when it's possible to compose styles, there have always been inconsistencies and unpredictable behaviour.

Yes, we are building a styling solution that is fast, scalable and easy to use. But we are also building a solution that actually provides predictable style composition without any nasty surprises.

· 4 min read
Naman Goel

StyleX v0.8.0 is now available with a bunch of fixes and new ESlint rules.

Linting Enhancements

We've been making a lot of improvements to our ESLint plugin. We've both improved our existing rules and added new ones. Thanks to Melissa Liu!

Here are some of the highlights:

New valid-shorthands rule

This rule enforces our opinions on when and how you should use CSS shorthand properties. It disallows the use of multi-value shorthands for shorthands, and disallows certain properties altogether.

const styles = stylex({
invalidShorthands: {
// border shorthands are entirely disallowed
// Use `borderWidth`, `borderStyle`, and `borderColor` instead
border: '1px solid black',
// Multiple values for different sides within the same shorthand are disallowed
borderWidth: '1px 2px 3px 4px',
margin: '10px 20px 30px',
padding: '10px 20px',
},
validShorthands: {
borderBottomWidth: 3,
borderColor: 'black',
borderInlineEndWidth: 2,
borderInlineStartWidth: 4,
borderStyle: 'solid',
borderTopWidth: 1,
marginBottom: 30,
marginInline: 20,
marginTop: 10,
paddingBlock: 10,
paddingInline: 20,
},
});

These opinions guide you towards the most consistent and most re-usable CSS.

tip

This rule offers an autofix for all disallowed properties.

New enforce-extension rule

This new rule enforces the rules when defining variables._createMdxContent It enforces that usages of stylex.defineVars are named exports within a file with a .stylex.js (or '.ts' or '.jsx' or '.tsx') extension, and that such a file does not have any other exports.

Other Lint fixes

We've updated the ESLint rule to include additional missing properties and values. Notably, the valid-styles rule should now understand:

  • fieldSizing as a valid prop
  • @starting-style as a valid at-rule.

Using lightningcss for post-processing

StyleX's compilation process is conceptually a three step process:

  1. Transform JavaScript source files to replace usages of stylex.create etc. with the resulting classNames and collect the generated CSS.
  2. De-duplicate and sort all the collected CSS.
  3. Write the CSS to a file.

However, often it's useful to post-process the CSS before writing it to a file. This post-processing can include minification, prefixing, or other transformations. After much discussion we have decided to standardize on lightningcss for this post-processing.

As a first step, we're add lightningcss by default for our Rollup Plugin. We will be rolling out support for our other bundler plugins next.

Thanks Prakshal Jain for his work on this!

Theming Improvements

We've made two small but important improvements for theming in StyleX.

Use stylex.firstThatWorks to define fallback values for CSS variables.

StyleX has a stylex.firstThatWorks function that can be used to define fallback values for CSS property. This is akin to using the same property multiple times with different values in CSS.

/* Represent this */
.my-class {
background-color: #808080;
background-color: oklab(0.5 0.5 0.5);
}
const styles = stylex.create({
myClass: {
// as:
backgroundColor: stylex.firstThatWorks(
'oklab(0.5 0.5 0.5)',
'#808080',
),
},
});

Now, the same API will also work for CSS variables.

/* Represent this */
.my-class {
background-color: var(--bg-color, #808080);
}
const styles = stylex.create({
myClass: {
// as:
backgroundColor: stylex.firstThatWorks(
'var(--bg-color)',
'#808080',
),
},
});

Themes have higher specificity than default var values

The CSS rule created with stylex.createTheme now has higher specificity than the rule created with stylex.defineVars.

This should not have been issue in the vast majority of cases already, as we always sorted the rules in the correct order. However, in extreme edge-cases where you may be loading multiple StyleX CSS files on the same page, this fix will ensure consistency.

Other fixes

We've made some other fixes in various parts of StyleX:

  • fix: Logical values for textAlign are no longer converted to left and right.
  • fix: [CLI] Handle errors while deleting files gracefully (#695)
  • feat: Expand configuration options to CLI (#638)
  • fix: Don't add 'px' units for number values used for variables (#694)
    • fix: Don't add 'px' units for additional properties that accept raw numbers as values (#705)

Documentation Improvements

We've added documentation for options of our various bundler plugin and added additional projects to our ecosystem page.

We've also updated the search on our website to be much more comprehensive and accurate. (Powered by Algolia)

· One min read
Naman Goel

StyleX v0.7.3 is now available with a fix to the Rollup plugin, which didn't previously include all the necessary files in the publish to NPM.

· 2 min read
Naman Goel

We're excited to release StyleX v0.7.0 with a new CLI to make it easier to get started with StyleX, improvements to variables, and various bug-fixes.

CLI

StyleX relies on a compiler that transforms your JavaScript code and generates a static CSS file. However, integrating this compiler with with your bundler can be tricky. So, while we continue to work on improving the quality of our bundler integrations, we are introducing a new CLI as an alternative!

The CLI transforms an entire folder. It generates an output folder where StyleX has already been compiled away and a static CSS file has been generated. Further, the CLI inserts an import statement for the generated CSS file into each file that uses StyleX.

We are excited to offer this alternative to the bundler-based setup and are choosing to release the CLI in an experimental state. We would love to hear your feedback on how it works for you and what improvements you would like to see.

Special thanks to Joel Austin for his work on the CLI.

Literal names for CSS variables

When using, stylex.defineVars, StyleX abstracts away the actual CSS variable name, and lets you use it as a JavaScript reference. Behind the scenes, unique variable names are generated for each variable.

However, there are scenarios where it is useful to know the exact variable name. For example, when you may want to use the variable in a standalone CSS file.

To address such use-cases, we have updated the stylex.defineVars API to use literals that start with -- as is. Other than the keys passed to stylex.defineVars, the API is unchanged.

const vars = stylex.defineVars({
'--primary-color': 'red',
'--secondary-color': 'blue',
});
Note

When using literals for variable names, StyleX cannot guarantee uniqueness.

Bug Fixes and improvements

Additionally bug fixes to types, eslint rules and the bundler plugins has been made.

· 3 min read
Naman Goel

We're excited to release StyleX v0.6.1 with some big improvements for working with CSS custom properties (aka "variables") as well as numerous bug-fixes.

Improvements for variables

We've added some new features and improvements for working with variables and themes in StyleX.

Fallback values for variables

You can now provide a fallback value for variables defined with the stylex.defineVars API. This new capability does not introduce any new API. Instead, the existing stylex.firstThatWorks API now supports variables as arguments.

import * as stylex from '@stylexjs/stylex';
import {colors} from './tokens.stylex';

const styles = stylex.create({
container: {
color: stylex.firstThatWorks(colors.primary, 'black'),
},
});

Using a list of fallbacks variables is supported.

Typed variables

StyleX introduces a brand new set of APIs for defining <syntax> types for CSS variables. This results in @property rules in the generated CSS output which can be used to animate CSS variables as well as other special use-cases.

The new stylex.types.* functions can be used when defining variables to define them with a particular type.

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

const typedTokens = stylex.defineVars({
bgColor: stylex.types.color<string>({
default: 'cyan',
[DARK]: 'navy',
}),
cornerRadius: stylex.types.length<string | number>({
default: '4px',
'@media (max-width: 600px)': 0,
}),
translucent: stylex.types.number<number>(0.5),
angle: stylex.types.angle<string>('0deg'),
shortAnimation: stylex.types.time<string>('0.5s'),
});

Once variables have been defined with types, they can be animated with stylex.keyframes just like any other CSS property.

import * as stylex from '@stylexjs/stylex';
import {typedTokens} from './tokens.stylex';

const rotate = stylex.keyframes({
from: { [typedTokens.angle]: '0deg' },
to: { [typedTokens.angle]: '360deg' },
});

const styles = stylex.create({
gradient: {
backgroundImage: `conic-gradient(from ${tokens.angle}, ...colors)`,
animationName: rotate,
animationDuration: '10s',
animationTimingFunction: 'linear',
animationIterationCount: 'infinite',
},
})

This can be used achieve some interesting effects, such as animating the angle of a conic-gradient:

This new capability is primarily about CSS types, but the new API also makes the TypeScript (or Flow) types for the variables more powerful.

As can be seen in the example, generic type arguments can be used to constrain the values the variable can take when creating themes with stylex.createTheme.

ESlint plugin

New sort-keys rule

We've added a new sort-keys rule to the StyleX ESlint plugin. This rule is a stylistic rule to enforce a consistent order for keys for your StyleX styles.

Thanks nedjulius

Improvements to propLimits for valid-styles rule.

The valid-styles rule has been improved to allow more expressive "prop limits".

Miscellaneous

  • ESlint 'valid-styles' rule now allows using variables created with stylex.defineVars as keys within dynamic styles.
  • Bug-fixes to the experimental stylex.include API
  • Fixed debug className generation for stylex.createTheme
  • Units are no longer removed from 0 values
  • Compilation bug-fixes

Our Ecosystem page continues to grow with community projects. Including a Prettier plugin for sorting StyleX styles.

· 2 min read
Naman Goel

We're excited to release Stylex v0.5.0 with some big improvements and fixes!

New stylex.attrs function

The stylex.props function returns an object with a className string and a style object. Some frameworks may expect class instead of className and a string value for style.

We are introducing a new stylex.attrs function so StyleX works well in more places. stylex.attrs returns an object with a class string and a style string.

New sort-keys rule for the Eslint plugin

A new @stylexjs/sort-keys plugin has been introduced which will sort styles alphabetically and by priority. This will make media query order more predictable.

Thanks @nedjulius!

New aliases option for the StyleX Babel plugin

A new aliases field can be used to configure StyleX to resolve custom aliases that may be set up in your tsconfig file. NOTE: StyleX currently needs to be configured with absolute paths for your aliases.

Thanks @rayan1810!

New Esbuild plugin

A new official plugin for Esbuild has been introduced as @stylexjs/esbuild-plugin.

Thanks @nedjulius!

Other Enhancements

  • Configuration options passed to the StyleX Babel plugin will now be validated.
  • The @stylexjs/stylex now has ESM exports alongside the commonJS exports.
  • The ESLint valid-styles rule will catch when using empty strings as string values.

Bug Fixes

  • Some CSS properties which previously caused type and lint errors will now be accepted.
  • Using variables for opacity will no longer cause type errors.
  • Using stylex.keyframes within stylex.defineVars will now work correctly
  • runtimeInjection will correctly be handled
  • Setting the value of variables from defineVars as dynamic styles will now work correctly.
  • Usage of 0px within CSS functions will no longer be simplified to a unit-less 0 as this doesn't work in certain cases.
  • Spaces around CSS operators will be maintained.

In addition to these, we've added an "Ecosystem" page to our website to highlight various community projects around StyleX.

· 2 min read
Naman Goel

Three weeks ago, we open-sourced StyleX. Since then, we've been diligently fixing bugs and making improvements. Here are some of the highlights:

Enhancements

  • The amount of JavaScript generated after compilation has been further reduced.
  • Added support for some previously missing CSS properties to the ESLint plugin.
  • Added support for using variables in stylex.keyframes.
  • Removed the code for style injection from the production runtime, reducing the size of the runtime by over 50%.
  • Added Flow and TypeScript types for the Rollup Plugin.
  • Added the option to use CSS Layers in all bundler plugins.
  • TypeScript will now auto-complete style property names.
  • Bundler plugins will now skip files that don't contain StyleX, resulting in faster build times.

Bug Fixes

  • Fixed a bug where the ESLint plugin was sometimes unable to resolve local constants used for Media Queries and Pseudo Classes.
  • Resolved a bug where the runtime injection of styles in dev mode would sometimes fail.
  • Addressed a bug where styles injected at runtime during development would sometimes suffer from specificity conflicts.
  • The TypeScript types for Theme will now correctly throw an error when applying a theme for the wrong VarGroup.

In addition to these, we've made other improvements to the types and documentation. I want to extend my gratitude to all the contributors for their pull requests. ♥️

Happy New Year!

· 7 min read
Naman Goel
Nicolas Gallagher

We are thrilled to introduce StyleX. StyleX is an expressive, deterministic, reliable, and scalable styling system for ambitious applications. We've taken the best ideas from the styling libraries that have come before to create something that is simultaneously familiar and uniquely new.

What is StyleX?

StyleX takes the developer experience of CSS-in-JS libraries and uses compile-time tooling to bridge it with the performance and scalability of static CSS. However, StyleX is not just another compiler-based CSS-in-JS library. StyleX has been carefully designed to meet the requirements of large applications, reusable component libraries, and statically typed codebases.

  1. StyleX supports an expressive subset of CSS. It avoids complex selectors and guarantees no specificity conflicts in the generated CSS.
  2. StyleX transforms, organizes, and optimizes styles into "atomic" CSS class names. There's no need to learn or manage a separate library of utility class names.
  3. StyleX allows styles to be merged across file and component boundaries, making it ideal for component libraries that allow user customization.
  4. StyleX is fully typed and provides type utilities to allow fine-grained control over what properties and values can be accepted by a component.

What are the advantages of StyleX?

Fast

StyleX is designed to be fast at both compile-time and runtime. The Babel transforms do not significantly slow down builds.

At runtime, StyleX entirely avoids the costs associated with using JavaScript to insert styles at runtime, and does little more than efficiently combine class name strings when necessary. And the generated CSS is optimized for size, ensuring that the styles for even the largest websites can be quickly parsed by browsers.

Scalable

StyleX is designed to scale to extremely large codebases, like the ones we have at Meta. The Babel plugin can handle processing styles in many thousands of components at compile-time by leveraging atomic builds and file-level caching. And because StyleX is designed to encapsulate styles, it allows new components to be developed in isolation with the expectation that they will render predictably once used within other components.

By generating atomic CSS class names, StyleX helps minimize the size of the CSS bundle. As the number of components in an application grows, the size of the CSS bundle starts to plateau. This frees developers from having to manually optimize or lazy-load CSS files.

Predictable

StyleX automatically manages the specificity of CSS selectors to guarantee that there are no collisions between the generated rules. StyleX gives developers a system that reliably applies styles, and ensures that "the last style applied always wins"

Composable

StyleX styles are easy to compose. Not only can multiple local styles be applied conditionally, styles can also be passed across files and components. Styles always merge with predictable results.

Type-Safe

You can constrain the styles a component accepts by using TypeScript or Flow types. Every style property and variable is fully typed.

Colocation

StyleX allows and encourages authoring styles in the same file as the component that uses them. This co-location helps make styles more readable and maintainable in the long run. StyleX is able to use static analysis and build-time tools to de-duplicate styles across components and to remove unused styles.

Testable

StyleX can be configured to output debug class names instead of functional atomic class names. This can be used to generate snapshots that don't change as often in response to minor design changes.

How does StyleX work?

StyleX is a collection of tools that work together.

  • A Babel plugin
  • A small runtime library
  • An ESlint plugin
  • A growing collection of integrations with bundlers and frameworks.

The most important part of StyleX is the Babel plugin. It finds and extracts all the styles defined within your source code and converts them to atomic class names at compile time. A helper function deduplicates, sorts, and writes the collected styles to a CSS file. These tools are used to implement bundler plugins.

To make using StyleX feel as natural as possible, StyleX supports various static patterns to define your styles by using local constants and expressions. Additionally, in order to give you the best performance possible, the Babel plugin also pre-computes the final class names when possible to remove any runtime cost — even merging class names — from a given file. If a component is defining and using styles within the same file statically, the runtime cost will be ZERO.

When using more powerful patterns such as style composition, a tiny runtime merges objects of class names dynamically. This runtime has been optimized to be extremely fast and the results are then memoized.

The origins of StyleX

The previous Facebook website used something akin to CSS modules and suffered from various problems that inspired the initial idea for CSS-in-JS. The average visitor to facebook.com would download tens of megabytes of CSS. Much of it unused. In order to optimize the initial load, we would lazy load our CSS which would, in turn, lead to slow update (or "Interaction to Next Paint") times. Usage of complex selectors would lead to conflicts or "specificity wars". Engineers would often resort to using !important or more complex selectors to solve their problems, making the entire system progressively worse.

A few years ago, when we were rebuilding facebook.com from the ground up using React, we knew we needed something better and built StyleX.

StyleX was designed to scale, and the design has proven itself in our years of experience using it. We've added new features to StyleX without regressing on performance or scalability while making StyleX more of a joy to use.

Using StyleX has been a massive improvement in both scalability and expressivity for us at Meta. On facebook.com we were able to bring down our CSS bundle from tens of megabytes of lazy-loaded CSS to a single bundle of a couple hundred kilobytes.

We created StyleX not only to meet the styling needs of React developers on the web, but to unify styling for React across web and native.

How does Meta use StyleX?

StyleX has become the preferred way to style components for every web surface within Meta. StyleX is used to style React components for every major external and internal product at Meta including Facebook, WhatsApp, Instagram, Workplace, and Threads. It has changed the way we author components, and resolved the issues our teams previously had with not being able to encapsulate and scale their styled components.

We expanded the original capabilities of StyleX so that engineers at Meta can use StyleX to author both static and dynamic styles. Our teams are using StyleX theming APIs to develop "universal" components that are themed to take on the appearance of different design systems used within different Meta products. And we're gradually expanding support for cross-platform styling, thanks to StyleX being aligned with the principles of encapsulation introduced by React Native's styling system.

Open Source

What we're open sourcing is what we use internally. We develop on Github first and sync it back to Meta. Although StyleX was originally created at Meta for Meta, it is not specific to Meta.

That said, this is still just the beginning. We look forward to working with the community to introduce further optimizations and more integrations.

We hope you love using StyleX as much as we do. ❤️