Exploring StyleX and the new generation of styling libraries
Tags: typescript, webdev
Published: 2024-03-06 15:21:39

**Written by [Ibadehin Mojeed](https://blog.logrocket.com/author/ibadehinmojeed/)✏️**

In the vast and dynamic world of web development, being ahead of the curve isn't just an advantage —it's essential. Over the years, CSS has evolved and continues to shape the visual landscape of the web. However, just like every technology, as CSS evolves, so do the tools at our disposal. 

In this guide, we'll venture into the realm of styling libraries with a special focus on StyleX. We’ll cover what you need to know about StyleX, including its benefits and drawbacks, so you can decide if it is the right solution for you. 

You can check out the [project code in this GitHub repo](https://github.com/Ibaslogic/stylex-vite) to see the code examples we’ll explore in this tutorial. Let’s get started.

## Understanding development trends

In response to the ever-evolving state of CSS, it’s crucial to know when to use a simplified solution versus embracing complex frameworks. 

For example, when a project demands a high level of organization and scalability, developers frequently turn to complex frameworks. However, for quicker development, particularly in the context of smaller projects, developers tend to favor a more decentralized approach. 

We should be able to access and adjust our preferred solutions based on the project requirements we are working on. Navigating this cyclical nature is crucial to stay current and effective.

## CSS and existing libraries

Before we explore StyleX, let's discuss the challenges of plain CSS and the issues it presents, and also those associated with existing libraries.

### Understanding the pain points of plain CSS

While native CSS has grown to include [features like nesting](https://blog.logrocket.com/native-css-nesting/), it still presents challenges that hinder ease in styling. 

Consider the following example. Which color do you think will apply to the heading text? Take a look at the HTML first:

    <h1 class="red blue">Text</h1>

Then the CSS:

    .blue {
      color: blue;
    }

    .red {
      color: red
    }

In the code, the styles for the `red` and `blue` classes conflict, potentially leading to the element rendering in ways we don’t intend. 

Since both selectors have the same specificity, the [order of classes in the CSS file takes precedence](https://blog.logrocket.com/how-css-works-understanding-the-cascade-d181cd89a4d8/) over their appearance in the HTML class attribute. This behavior might seem unintuitive, resulting in the application of the `red` color when we really want `blue`. 

If the classes were to be split across two stylesheets, the precedence now depends on the order in which they are inserted into the page. This lack of predictability can lead to unexpected results which can make it harder to manage styles, especially in larger projects. 

Other issues that may arise with plain CSS include naming conflicts with global scoping, specificity problems, and more. While we can address these issues by following a more [structured approach like BEM](https://blog.logrocket.com/bem-vs-smacss-comparing-css-methodologies/), it can lead to bloated CSS.

### CSS/Sass Module

[CSS Module](https://blog.logrocket.com/a-deep-dive-into-css-modules/) solves the naming conflicts and helps avoid global scope issues associated with the plain CSS. It allows developers to create self-contained components with their styling to enhance code organization. 

However, one of the drawbacks of CSS Modules is that they don’t prevent duplicate CSS definitions across different files, which may lead to redundant styles.

### Tailwind CSS

[Tailwind CSS](https://blog.logrocket.com/how-to-use-tailwind-css-react-vue-js/), known for its utility-first approach, has been the go-to styling solution for many developers. It helps address the cons of the previous solution — for example, it mitigates issues with redundant styles by automatically purging unused styles. 

In addition, Tailwind CSS allows for rapid prototyping by leveraging ready-made utility classes. Likewise, it promotes the collocation of styles with HTML components — which helps with code reviews — and the styling of individual components without navigating between CSS files. 

While this approach tends to improve the developer experience, it can however lead to bloated HTML and JSX code if not careful. Another minor drawback of this approach is the steep learning curve — you have to learn an abstraction of CSS.

### CSS-in-JS solutions

CSS-in-JS is popular when building component-based projects like React. This approach offers various advantages, such as improved component encapsulation and allowing the app’s CSS to exploit the full expressive power of JavaScript. 

CSS-in-JS solutions can be categorized into runtime and build-time solutions:

*   Runtime solutions like Emotions and [styled-components](https://blog.logrocket.com/using-styled-components-react/) will parse and inject styles during runtime. However, this can introduce runtime overhead that may cause performance bottlenecks
*   [Build-time solutions like vanilla-extract](https://blog.logrocket.com/vanilla-extract-tutorial-create-zero-runtime-stylesheets-in-typescript/) and Linaria address this problem by extracting the CSS from JavaScript or TypeScript files into a static CSS file at build time, eliminating the bottlenecks at runtime.

While these libraries have similar principles, their approaches are different. For example, vanilla-extract leverages TypeScript to provide type-safe styles, but Linaria lacks type-checking for CSS properties. 

Conversely, vanilla-extract — unlike Linaria — forces CSS styles to stay inside a separate `.css.ts` file. This removes the benefits of collocation, as we tend to navigate between CSS and the component file.

## What is StyleX?

As we've briefly explored, each of the solutions mentioned above comes with its distinct features, advantages, and trade-offs. Now, the question: what does StyleX have to offer? 

StyleX is a build-time, type-safe CSS-in-JS library that was recently [open sourced by Meta](https://github.com/facebook/stylex). The Meta team developed this library to address some of the major challenges faced by large-scale enterprise projects. <iframe style="border-radius:12px" src="https://open.spotify.com/embed/episode/094Q7XUIXnaGjWKVmfMliF?utm_source=generator" width="100%" height="152" frameborder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>

Earlier, we mentioned how CSS-in-JS helps improve component encapsulation. This pattern also allows modularity and composability of components. In turn, it allows for the reusability of UI code across projects. While that’s fine, it can be challenging to customize predefined styles. 

That’s where StyleX comes in. It shines in being able to predictably merge and compose styles across packages.

## Simple demonstration of StyleX

We’ll use Vite for this practical walkthrough. So, go ahead and [install Vite](https://blog.logrocket.com/build-react-typescript-app-vite/) first. Then, install the StyleX runtime package:

```bash
npm install --save @stylexjs/stylex
```

[Depending on the bundler you’re using](https://stylexjs.com/docs/learn/installation/), we need to install a plugin for integrating StyleX. For Vite, let’s install the following plugin:

```bash
npm install --save-dev vite-plugin-stylex
```

Then add the plugin to your Vite config file (`vite.config.ts`):

```typescript
// ... other imports

import styleX from "vite-plugin-stylex";

export default defineConfig({
  plugins: [react(), styleX()],
});
```

### StyleX syntax and usage

Let's begin by using two StyleX APIs: `stylex.create()` to establish style rules and `stylex.props()` to apply those styles to elements. 

We’ll import `stylex` and use it like so:

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

const styles = stylex.create({
  base: {
    color: 'blue',
    fontSize: 30,
  },
});

export function SimpleText() {
  return <h1 {...stylex.props(styles.base)}>I am a heading text</h1>;
}
```

The result should look like so: ![Demo Of Simple Text Styled With Stylex](https://blog.logrocket.com/wp-content/uploads/2024/02/img1-Demo-simple-StyleX-text-styles.png) The `stylex.create()` generates collision-free atomic CSS whose style rules are extracted into a static file at build time. Then, all we have left is the optimized component and the generated atomic CSS in a separate file. We’ve eliminated the runtime costs of CSS-in-JS and retained compatibility with SSR. 

If we run the `npm run build` command, we’ll generate a build folder containing production-ready files, including the static CSS file: ![Screenshot Of Build Folder Generated With Production Ready Stylex Files](https://blog.logrocket.com/wp-content/uploads/2024/02/img2-Screenshot-build-folder-production-ready-files.png) This implementation ensures that CSS and JavaScript resources load in parallel, providing a performance boost. With the atomic CSS approach, StyleX can gain additional performance benefits by minimizing the overall size of the CSS bundle.

### Checking for conflicting styles

If we include additional keys in the `stylex.create()`, StyleX will take into consideration the order in which styles are applied to the element and not how styles are defined:

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

const styles = stylex.create({
  colorRed: {
    color: 'red',
  },
  base: {
    color: 'blue',
    fontSize: 30,
  },
});

export function SimpleText() {
  return (
    <h1 {...stylex.props(styles.base, styles.colorRed)}>
      I am a heading text
    </h1>
  );
}
```

This makes StyleX predictable and intuitive. So, in the above code, the last style applied wins! Take a look: ![Simple Heading Text With Red Color Applied With Stylex](https://blog.logrocket.com/wp-content/uploads/2024/02/img3-Heading-text-red-color-applied.png) No more worries about conflicting style rules.

## Leveraging StyleX for more complex styling needs

One thing that comes to mind — especially if you have used some other libraries like Tailwind CSS — is that StyleX seems more complicated than other styling solutions. However, the benefits are obvious when used alongside reusable UI components or when you work on a more complex design system and style variants. 

Let’s see how we can write some more complicated code by styling a reusable UI component.

### Reusable `Button` component

The following code defines a reusable `Button` component:

```typescript
import { ComponentProps } from 'react';

type CustomButtonProps = {} & ComponentProps<'button'>;

export function Button({ ...props }: CustomButtonProps) {
  return <button {...props} />;
}
```

For type safety, we’ve leveraged the `ComponentProps` type to inherit the standard `button` element props — like `onCLick` and `className` — while also allowing for additional custom props if needed. We can then render the component like so:

```html
<Button>Button</Button>
```

Note that we haven’t used StyleX yet! We’ll address that next.

### Applying default styles

Let’s apply default StyleX styles to the button:

```typescript
// ... other imports
import * as stylex from '@stylexjs/stylex';

type CustomButtonProps = {} & ComponentProps<'button'>;

const btnStyles = stylex.create({
  default: {
    color: '#fff',
    border: 'none',
    backgroundColor: '#0f172a',
    borderRadius: '.25rem',
    height: '2.5rem',
    padding: '0.5rem 1rem',
    cursor: 'pointer',
  },
});

export function Button({ ...props }: CustomButtonProps) {
  return <button {...stylex.props(btnStyles.default)} {...props} />;
}
```

The button should now look like so: ![Example Button Component Created With Stylex](https://blog.logrocket.com/wp-content/uploads/2024/02/img4-Example-button-component-created-StyleX.png)

### Applying variants and conditional styling

Variants offer a means to adjust the appearance of our button dynamically depending on specific conditions. In our example, we will define variants such as `outline`, `destructive`, and `ghost`. This flexibility ensures a dynamic and personalized user interface. 

Let’s define each variant alongside the default namespace:

```typescript
const btnStyles = stylex.create({
  // ...default style here

  outline: {
    color: '#000',
    backgroundColor: '#feffff',
    border: '1px solid #dbdbdb',
  },
  destructive: {
    backgroundColor: '#f15756',
  },
  ghost: {
    color: '#000',
    backgroundColor: 'transparent',
  },
});
```

Next, we can apply the relevant styles by using a `variant` prop as a key within the `btnStyles` object. If `variant` is not provided, we retain the default styles:

```typescript
type CustomButtonProps = {
  variant?: 'outline' | 'destructive' | 'ghost';
} & ComponentProps<'button'>;

const btnStyles = stylex.create({
  // styles...
});

export function Button({ variant, ...props }: CustomButtonProps) {
  return (
    <button
      {...stylex.props(
        btnStyles.default,
        variant && btnStyles[variant]
      )}
      {...props}
    />
  );
}
```

In `stylex.props()`, we utilize the `&&` operator to conditionally apply styles when the `variant` prop is provided. These styles will merge with the default ones, resulting in the intended visual output. 

This is a common challenge for many libraries. Tailwind CSS, for example, struggles to effectively merge classes, leading to unintended behavior. However, Tailwind developers often turn to third-party solutions such as `tailwind-merge` to overcome the obstacle. 

If we add the `variant` prop to our component elements:

```html
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
```

The result should look like so: ![Default Button Style Shown Next To Destructive, Ghost, And Outline Variants](https://blog.logrocket.com/wp-content/uploads/2024/02/img5-Button-variants-created-StyleX.png)

### Allowing for custom styles

Unlike the majority of libraries, StyleX simplifies the process for end users to override predefined component styles. It can intelligently merge styles across component boundaries. 

In the `Button` component, we will pass a `styles` prop and append it after the local styles in the `stylex.props()` function:

```typescript
type CustomButtonProps = {
  variant?: 'outline' | 'destructive' | 'ghost';
  styles?: stylex.StyleXStyles;
} & ComponentProps<'button'>;

const btnStyles = stylex.create({
  // ...
});

export function Button({
  variant,
  styles,
  ...props
}: CustomButtonProps) {
  return (
    <button
      {...stylex.props(
        btnStyles.default,
        variant && btnStyles[variant],
        styles
      )}
      {...props}
    />
  );
}
```

We’ve named the prop `styles`, but you can name whatever you’d like. Notice how we also utilized the `StyleXStyles` to accept any arbitrary StyleX styles. 

Now, we can pass a custom style to the `Button` component:

```typescript
const styles = stylex.create({
  button: {
    backgroundColor: 'red',
  },
});

export default function App() {
  return (
    <Button variant="destructive" styles={styles.button}>
      Destructive
    </Button>
  );
}
```

### Constraining accepted styles

StyleX simplifies the process of limiting the styles that can be passed to a component. If we specifically want only certain styles to be allowed, we can pass an object type containing the desired properties to `StyleXStyles<{...}>`:

```typescript
styles?: stylex.StyleXStyles<{
  color?: string;
  backgroundColor?: string;
}>;
```

In this instance, we can only supply the `color` and `backgroundColor` to the component as follows:

```typescript
const styles = stylex.create({
  button: {
    color: 'blue',
    backgroundColor: 'red',
  },
});
```

Trying to pass other styles like `fontSize` as seen in the code below will result in a type error:

```typescript
const styles = stylex.create({
  button: {
    // ...
    fontSize: 30
  },
});
```

Conversely, rather than allowing certain styles with `StyleXStyles`, we may want to disallow specific properties with `StyleXStylesWithout`:

```typescript
styles?: stylex.StyleXStylesWithout<{
  backgroundColor: unknown;
}>;
```

In this case, we can pass any StyleX properties except the `backgroundColor`. Otherwise, we’ll get a type error:

```typescript
const styles = stylex.create({
  button: {
    color: 'blue',
    // backgroundColor: 'red',
  },
});
```

This extra level of type safety is extremely nice!

### Nesting style values

To handle pseudo-selectors with StyleX, we can nest the selector within StyleX style properties. For instance, we can handle the `hover` state pseudo-class for our reusable `Button` component like so:

```typescript
const btnStyles = stylex.create({
  default: {
    // ...
    opacity: {
      default: 1,
      ':hover': 0.8,
    },
  },
  outline: {
    color: '#000',
    backgroundColor: {
      default: '#feffff',
      ':hover': '#f3f3f3',
    },
    border: '1px solid #dbdbdb',
  },
  destructive: {
    backgroundColor: '#f15756',
  },
  ghost: {
    color: '#000',
    backgroundColor: {
      default: 'transparent',
      ':hover': '#f3f3f3',
    },
  },
});
```

In the `default` namespace, we added a pseudo-class to change the opacity on hover. This applies to the button across various scenarios. We also targeted `outline` and `ghost` namespaces and applied pseudo-classes to change the background color on hover. 

In the same way, we can apply pseudo-elements and media queries:

```typescript
const styles = stylex.create({
  button: {
    width: {
      default: 200,
      '@media (max-width: 400px)': '100%',
    },
  },
});
```

### Using CSS variables

StyleX lets us define custom properties using `stylex.defineVars` API in a specialized `.stylex.ts` or `.stylex.js` file. For instance, we can create a `tokens.stylex.ts` file and define our variables:

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

const DARK = '@media (prefers-color-scheme: dark)';

export const colors = stylex.defineVars({
  primaryColor: { default: 'white', [DARK]: 'black' },
  primaryDarkColor: { default: 'black', [DARK]: 'white' },
  lightGreyColor: { default: '#f3f3f3', [DARK]: '#605e5e' },
});

export const spacing = stylex.defineVars({
  sizeSm: '.25rem',
  sizeXl: '2.5rem',
});
```

We’ve defined different values for the variables based on the user’s or device’s preferred color scheme. StyleX will handle the `stylex.defineVars` at compile time, generating CSS variable names for the corresponding tokens automatically. 

To use the variables, we can import them and use them within `stylex.create`:

```typescript
import { colors, spacing } from '../../tokens.stylex';
// ...
const btnStyles = stylex.create({
  default: {
    color: colors.primaryColor,
    border: 'none',
    backgroundColor: colors.primaryDarkColor,
    borderRadius: spacing.sizeSm,
    height: spacing.sizeXl,
    // ...
  },
  outline: {
    color: colors.primaryDarkColor,
    backgroundColor: {
      default: colors.primaryColor,
      ':hover': colors.lightGreyColor,
    },
    border: '1px solid #dbdbdb',
  },
  destructive: {
    backgroundColor: '#f15756',
  },
  ghost: {
    color: colors.primaryDarkColor,
    // ...
  },
});
```

### Dynamic styles

StyleX also supports dynamic styling at runtime, drawing inspiration from [Linaria’s approach to generating CSS custom properties](https://blog.logrocket.com/using-linaria-faster-css-in-js-react-apps/). 

To implement dynamic styling, we define styles as a function and pass in the dynamic value. In the following code, we utilize the component state to determine the `Button`’s opacity, simulating asynchronous form submission:

```typescript
const styles = stylex.create({
  // ...
  dynamicStyle: (value) => ({
    opacity: value,
  }),
});

export default function App() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleButtonClick = async () => {
    // Simulate an asynchronous form submission
    setIsSubmitting(true);
    await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating a delay
    setIsSubmitting(false);
  };

  return (
    <div>
      {/* ... */}
      <Button
        onClick={handleButtonClick}
        styles={styles.dynamicStyle(isSubmitting ? 0.5 : 1)}
      >
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </Button>
    </div>
  );
}
```

See the result below: ![Example Of Dynamic Styles Created With Stylex](https://blog.logrocket.com/wp-content/uploads/2024/02/img6-Dynamic-styles-example.gif) As expected, StyleX will generate static styles, but will now depend on a CSS variable. This variable is dynamically updated at runtime, as seen in the GIF above.

## StyleX: A Tailwind killer?

There is a misconception in some quarters that StyleX is positioned as a Tailwind killer. However, this assertion is inaccurate. StyleX does not aim to replace Tailwind — rather, it serves a different purpose in the realm of styling. 

While Tailwind excels in facilitating quickstarts and works well for standalone projects, StyleX addresses significant challenges commonly encountered in large-scale enterprise projects. Specifically, StyleX deals with the ability to seamlessly predict, merge, and compose styles across packages. 

StyleX accommodates individuals who may not align with Tailwind's approach but are looking for a type-safe CSS-in-JS solution without incurring runtime overhead.

## Conclusion

As CSS undergoes continual transformation, our tools also keep getting updated. This guide delved into styling libraries, placing a spotlight on StyleX. 

Throughout this tutorial, we discussed the nuances of StyleX, examining its advantages and drawbacks. With this understanding, you are now equipped to make an informed decision on whether StyleX is the right solution for your needs in the ever-changing world of web styling. 

If you have questions or contributions, share your thoughts in the comment section. And if you enjoyed the article, share it with the world. 

See the project [code on GitHub](https://github.com/Ibaslogic/stylex-vite).

---

[LogRocket](https://lp.logrocket.com/blg/typescript-signup): Full visibility into your web and mobile apps

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/typescript-signup)

[LogRocket](https://lp.logrocket.com/blg/typescript-signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free](https://lp.logrocket.com/blg/typescript-signup).