DEV Community

Cover image for Exploring StyleX and the new generation of styling libraries
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Exploring StyleX and the new generation of styling libraries

Written by Ibadehin Mojeed✏️

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 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, 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>
Enter fullscreen mode Exit fullscreen mode

Then the CSS:

.blue {
  color: blue;
}

.red {
  color: red
}
Enter fullscreen mode Exit fullscreen mode

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 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, it can lead to bloated CSS.

CSS/Sass Module

CSS Module 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, 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 will parse and inject styles during runtime. However, this can introduce runtime overhead that may cause performance bottlenecks
  • Build-time solutions like vanilla-extract 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. The Meta team developed this library to address some of the major challenges faced by large-scale enterprise projects.

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 first. Then, install the StyleX runtime package:

npm install --save @stylexjs/stylex
Enter fullscreen mode Exit fullscreen mode

Depending on the bundler you’re using, we need to install a plugin for integrating StyleX. For Vite, let’s install the following plugin:

npm install --save-dev vite-plugin-stylex
Enter fullscreen mode Exit fullscreen mode

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

// ... other imports

import styleX from "vite-plugin-stylex";

export default defineConfig({
  plugins: [react(), styleX()],
});
Enter fullscreen mode Exit fullscreen mode

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:

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>;
}
Enter fullscreen mode Exit fullscreen mode

The result should look like so: Demo Of Simple Text Styled With Stylex 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 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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 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:

import { ComponentProps } from 'react';

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

export function Button({ ...props }: CustomButtonProps) {
  return <button {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

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:

<Button>Button</Button>
Enter fullscreen mode Exit fullscreen mode

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:

// ... 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} />;
}
Enter fullscreen mode Exit fullscreen mode

The button should now look like so: Example Button Component Created With Stylex

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:

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

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

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:

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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
Enter fullscreen mode Exit fullscreen mode

The result should look like so: Default Button Style Shown Next To Destructive, Ghost, And Outline Variants

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:

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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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<{...}>:

styles?: stylex.StyleXStyles<{
  color?: string;
  backgroundColor?: string;
}>;
Enter fullscreen mode Exit fullscreen mode

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

const styles = stylex.create({
  button: {
    color: 'blue',
    backgroundColor: 'red',
  },
});
Enter fullscreen mode Exit fullscreen mode

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

const styles = stylex.create({
  button: {
    // ...
    fontSize: 30
  },
});
Enter fullscreen mode Exit fullscreen mode

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

styles?: stylex.StyleXStylesWithout<{
  backgroundColor: unknown;
}>;
Enter fullscreen mode Exit fullscreen mode

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

const styles = stylex.create({
  button: {
    color: 'blue',
    // backgroundColor: 'red',
  },
});
Enter fullscreen mode Exit fullscreen mode

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:

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',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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',
});
Enter fullscreen mode Exit fullscreen mode

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:

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,
    // ...
  },
});
Enter fullscreen mode Exit fullscreen mode

Dynamic styles

StyleX also supports dynamic styling at runtime, drawing inspiration from Linaria’s approach to generating CSS custom properties.

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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

See the result below: Example Of Dynamic Styles Created With Stylex 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.


LogRocket: Full visibility into your web and mobile apps

LogRocket Signup

LogRocket 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.

Top comments (0)