Decoding StyleX: Meta's Cutting-Edge Styling System

Decoding StyleX: Meta's Cutting-Edge Styling System

ยท

11 min read

Every October, the biggest international react conference occurs in Goa, India. Yes, I am talking about React India. This year (2024), was even more special for me as I got a chance to speak at this magnificent conference. Here's the recording of my talk if you've missed to watch it live. If you prefer reading over watching videos, this blog is just for you! Let's dive into it.

What is StyleX?

StyleX is Meta's new, scalable styling library that is now used as the primary system behind platforms like Facebook, Instagram, and WhatsApp. It addresses the pain points experienced with CSS-in-JS approaches, particularly in massive React applications. By offering a hybrid solution that blends the best features of both atomic CSS and static CSS, StyleX offers an efficient, modular, and scalable alternative.

How and Why did Meta create StyleX?

  • Meta built StyleX to address specific challenges encountered with traditional CSS-in-JS libraries in large-scale projects:

    1. Unused Styles: As projects grow, CSS often accumulates unused rules, bloating the stylesheet.

    2. Performance Issues: CSS-in-JS solutions can result in large CSS files or performance bottlenecks, especially when bundled with the application.

    3. CSS-in-JS Library Size: Many popular libraries used for styling in JavaScript add unnecessary weight to the bundle, impacting load times.

  • Introduction of StyleX: It was created in 2019 as part of a Facebook UI revamp and they made it open-source in December 2023.

  • CSS Optimization: Before using StyleX, a single page on Facebook would load around 15-45MB of CSS styles. This was drastically reduced to around 200-300KB with StyleX by utilizing a single CSS bundle.

  • Purpose of StyleX: It was developed to effectively manage the complexities of styling at scale. It addresses the challenges that arise when numerous developers create thousands of components, which often leads to specificity conflicts within CSS. By providing a structured framework for styling, StyleX helps maintain consistency and clarity in the styling process.

  • Atomic Class Generation: From the outset, StyleX consistently generates atomic classes, accepting the trade-off of having multiple class names per component for improved maintainability and reduced styling conflicts.

Key Features of StyleX:

  1. Atomic CSS Generation: StyleX employs atomic CSS generation, which means it creates small, reusable classes for each style rule. This approach not only minimizes redundancy in the final CSS bundle but also improves performance by reducing the overall size of the stylesheets.

  2. CSS Deduplication: By generating unique class identifiers for each style, StyleX effectively eliminates duplicate styles. This deduplication process ensures that each property-value pair is rendered only once, further contributing to a leaner CSS output.

  3. โ€œThe Last Style Applied Always Wins!โ€: StyleX follows a predictable styling rule where the last style applied takes precedence. This feature simplifies debugging and enhances developer confidence, as it mitigates concerns about conflicting style rules.

  4. Optimized for React: Designed specifically for React applications, StyleX integrates seamlessly into the React ecosystem. It allows developers to define styles directly within their components, fostering a more cohesive development workflow.

  5. Flow and TypeScript Support: StyleX is written in "Flow" (created by Meta) and it also provides robust support for TypeScript, enabling type-safe APIs for styles and themes. This type safety enhances code reliability and maintainability, making it easier to manage complex styling scenarios.

  6. Flexible Conditional Styling: With StyleX, developers can apply styles conditionally based on component states or props. This flexibility allows for dynamic styling that adapts to user interactions or changes in application state.

  7. Scoped Styling: The scoped styling feature of StyleX ensures that styles are applied only to the components they are intended for. This prevents unintended side effects and specificity issues that often arise in larger codebases.

  8. Fewer Runtime Calculations: StyleX minimizes runtime calculations by bundling all styles into a static CSS file at compile time. This optimization leads to faster rendering times and improved performance, especially in larger applications.

  9. Better Code Maintainability: By co-locating styles with their respective components and utilizing atomic classes, StyleX promotes better code maintainability. Developers can easily understand and modify styles without sifting through extensive stylesheets.

  10. Minimal CSS Output: The use of atomic CSS results in minimal CSS output, which is particularly beneficial for performance. As projects grow in size and complexity, StyleX ensures that the CSS bundle remains manageable without sacrificing functionality.

  11. Works Well for Projects of All Sizes: While StyleX is suitable for projects of all sizes, it truly excels in larger applications. Its architecture is designed to handle the complexities of extensive styling needs without compromising on performance or maintainability.

Let's see how it works ๐Ÿง‘โ€๐Ÿ’ป

The code examples in this article are written in React, and we will primarily work with two components, App.jsx and Button.jsx. Let's take a look at the basic structure of these components before we add styles.

import Button from "./components/Button";

const App = () => {
  return (
    <div>
      <h1>StyleX by Meta</h1>
      <Button text="Get Started" />
    </div>
  );
};

export default App;
// Button.jsx
import PropTypes from "prop-types";

const Button = ({ text }) => {
  return <button>{text}</button>;
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
};

export default Button;

Adding styles using StyleX

import PropTypes from "prop-types";
import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
  base: {
    fontSize: 18,
    backgroundColor: "black",
    color: "white",
  },
});

const Button = ({ text }) => {
  return <button {...stylex.props(styles.base)}>{text}</button>;
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
};

export default Button;

To use these styles, we need to import them from the StyleX package and then define the styles using stylex.create method that takes an object as a parameter. We can then use the stylex.props method to apply the styles to the component.

In this example, base is the name of the style that we want to apply. We call them namespaces in StyleX. This is how our button component looks like now.

Adding styles to pseudo-classes

import PropTypes from "prop-types";
import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
  base: {
    fontSize: 18,
    backgroundColor: {
      default: "black",
      ":hover": "blue",
    },
    color: "white",
  },
});

const Button = ({ text }) => {
  return <button {...stylex.props(styles.base)}>{text}</button>;
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
};

export default Button;

With StyleX, it's pretty simple to add styles to pseudo-classes. In the previous example, backgroundColor was a string. Here, we convert it to an object with the default value and a pseudo-class.

Working with media-queries

import PropTypes from "prop-types";
import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
  base: {
    fontSize: 18,
    backgroundColor: {
      default: "black",
      ":hover": "blue",
    },
    color: "white",
    width: {
      default: "100px",
      "@media (max-width: 476px)": "100%",
    },
  },
});

const Button = ({ text }) => {
  return <button {...stylex.props(styles.base)}>{text}</button>;
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
};

export default Button;

One thing that we do differently in StyleX when compared to other styling libraries is the media queries. Here, we apply media queries to every namespace based on requirements. In this example, we are defining the width of the button to be 100px for larger screens and 100% width for smaller screens or mobile devices.

Let's see how "last style applied always wins"

Let's extend the previous example to see how we can create different variants of this button.

const styles = stylex.create({
  base: {
    fontSize: 18,
    backgroundColor: {
      default: "teal",
      ":hover": "blue",
    },
    color: "white",
    width: {
      default: "100px",
      "@media (max-width: 476px)": "100%",
    },
  },
  highlighted: {
    backgroundColor: "orange",
  },
  danger: {
    backgroundColor: "red",
  },
  primary: {
    backgroundColor: "green",
  },
});

const Button = ({ text, isHighlighted, variant }) => {
  return (
    <button
      {...stylex.props(
        styles.base,
        isHighlighted && styles.highlighted, // conditional styling
        styles[variant]
      )}
    >
      {text}
    </button>
  );
};

Button.propTypes = {
  text: PropTypes.string.isRequired,
  isHighlighted: PropTypes.bool,
  variant: PropTypes.oneOf(["danger", "primary"]),
};

Let's add a few more namespaces to stylex.create method and provide them with different background colors. Additionally, we are accepting 2 new props within our Button component. isHighlighted is a boolean prop that we use to apply the highlighted namespace. And variant is a prop that we use to apply the primary, danger or highlighted namespace.

// App.jsx
import Button from "./components/Button";

const App = () => {
  return (
    <div>
      <h1>StyleX by Meta</h1>
      <div {...stylex.props(styles.main)}>
        <Button text="Base Button" />
        <Button text="Highlighted Button" isHighlighted />
        <Button text="Danger Button" isHighlighted variant="danger" />
        <Button text="Primary Button" variant="primary" />
      </div>
    </div>
  );
};

export default App;

We create a few more copies of the Button component with different props being passed on. This is how our app looks like now.

Now, take a closer look at 'Danger Button'. Even though we have passed in isHighlighted as true, the highlighted namespace will not be applied. The danger variant is mentioned last and so it will be applied. Thus, the button will have a red background color.

Overriding styles from parent

We could override the style properties of this Button component from App.jsx directly.

//App.jsx

import Button from "./components/Button";

const styles = stylex.create({
  override: {
    backgroundColor: "purple",
    color: "white",
  },
  main: {
    margin: "1rem",
    display: "flex",
    alignItems: "center",
    gap: "2rem",
  },
});

const App = () => {
  return (
    <div>
      <h1>StyleX by Meta</h1>
      <div {...stylex.props(styles.main)}>
        <Button text="Base Button" />
        <Button text="Highlighted Button" isHighlighted />
        <Button text="Danger Button" isHighlighted variant="danger" />
        <Button text="Primary Button" variant="primary" />
        <Button
          text="Overriden Button"
          isHighlighted
          variant="danger"
          style={styles.override}
        />
      </div>
    </div>
  );
};

In this example, we are overriding the backgroundColor and color properties of the danger variant of the Button component. We define a new namespace called override and pass in the properties that we want to override. This namespace is passed to style prop.

In our button component, we shall consume this namespace too.

// Button.jsx

const styles = stylex.create({
  base: {
    fontSize: 18,
    backgroundColor: {
      default: "teal",
      ":hover": "blue",
    },
    color: "white",
  },
  highlighted: {
    backgroundColor: "orange",
    margin: "1rem",
  },
  danger: {
    backgroundColor: "red",
  },
  primary: {
    backgroundColor: "green",
    margin: "1rem",
  },
});

const Button = ({ text, isHighlighted, variant, style }) => {
  return (
    <button
      {...stylex.props(
        styles.base,
        isHighlighted && styles.highlighted,
        styles[variant],
        style
      )}
    >
      {text}
    </button>
  );
};

In this example, the override namespace currently allows any properties. However, StyleX gives us the capability to limit which properties can be overridden. This feature becomes particularly useful when using TypeScript.

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

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

This limitation ensures that only the backgroundColor and color properties can be overridden.

How does atomic classes work (internals)

If you scroll up to the previous example code, you will see that we have added margin: "1rem" style to 3 different namespaces - main in App.jsx, highlighted and primary in Button.jsx. When we inspect the element using Devtools, we can see that the different components (main container, highlighted button, and primary button) are attached with the same class name and there is only 1 class x42y017 that holds margin: "1rem" style.

That's how StyleX significantly reduced its bundle size by employing atomic classes. After reaching a certain threshold, no new classes are generated; instead, they simply reuse the existing classes.

Global Variables and Themes

Being able to override styles at a granular level is great! However, any given design systems need to support design tokens and themeing. That's where StyleX comes in. The design of the theming APIs in StyleX are directly inspired by React's Context APIs. Variables are defined with default values similar to how React Contexts are created, and themes can be created to โ€œprovideโ€ different values for these variables for UI sub-trees.

We can create global styles by creating a x.stylex.js file. Make sure to follow this naming convension. In this file, we make use of stylex.defineVars as shown below.

// tokens.stylex.js
import * as stylex from '@stylexjs/stylex';
export const DARK = '@media (prefers-color-scheme: dark)';

// This function is processed at compile-time and CSS variable names are automatically generated.
export const colors = stylex.defineVars({
  primaryText: { default: 'black', [DARK]: 'white' },
  background: { default: 'white', [DARK]: 'black' },
  borderRadius: '4pxโ€™,
  border: '4px solidโ€™,
  borderColor: { default: 'black', [DARK]: 'white' },
});

We are referring to the user's preferred theme and setting it to a constant value - DARK. Further, let's create a new theme using this colors variable.

// theme.js
import * as stylex from '@stylexjs/stylex';
import { colors, DARK } from './tokens.stylex';

// MyCustomTheme theme
export const myCustomTheme = stylex.createTheme(colors, {
  primaryText: { default: 'navy', [DARK]: 'cyan' },
  background: { default: '#ccc', [DARK]: 'black' },
  borderRadius: '2pxโ€™,
  border: '2px solidโ€™,
  borderColor: { default: 'navy', [DARK]: 'cyan' },
});

Once the theme is created, it can be used just like any other style in StyleX.

// App.js
import { colors } from "./tokens.stylex";
import { myCustomTheme } from "./theme";

const styles = stylex.create({
  container: {
    color: colors.primaryText,
    backgroundColor: colors.background,
    border: colors.border,
    borderRadius: colors.borderRadius,
  },
  // other namespaces
});

const App = () => {
  return (
    <div {...stylex.props(myCustomTheme, styles.container)}>
      <h1>StyleX by Meta</h1>
      <p>
        StyleX is a CSS-in-JS library that generates atomic CSS classes at
        compile-time.
      </p>
      .. .. .. Button container .. .. ..
    </div>
  );
};

That's how we can see the same page with myCustomTheme in light and dark mode respectively.

That's a wrap ๐ŸŽ‰

Hurray! We have successfully got a gist of working with StyleX. Thank you for reading through this article. I hope it provided a good understanding of what is StyleX, how did Meta create it, and how to use it. Please share your thoughts/queries in the comments section or on Twitter. If this blog is interesting to you, I would appreciate it if you could give this post a like (with your favorite emoji ๐Ÿ˜).

Peace โœŒ

References

Connect with me on TopMate for Interview Preparations

Buy Me A Coffee