CSS at Scale

How thousands of engineers at big tech companies write CSS.
Author
Yangshun Tay
7 min read
Dec 22, 2023
CSS at Scale

Writing CSS at scale, particularly in large and complex projects, can present several challenges. Here are some common problems associated with scaling CSS:

  • Global namespace: CSS operates in a global scope, which can lead to naming collisions and unintended style overrides. As a project grows, managing the global namespace becomes increasingly challenging, and conflicts between styles become more likely.
  • Specificity issues: CSS specificity determines which styles apply to an element. As the project scales, managing and understanding specificity can become complex, leading to unexpected styling outcomes and difficulties in maintaining a consistent design.
  • Unclear dependencies: CSS classes can be created on the fly, and you can't be sure where a style is being used and whether it's still being used.
  • Huge stylesheets: When it is hard to determine whether certain styles are still being used, stylesheets become “add-only”. This leads to redundant or unused styles, as well as overly specific selectors, which contribute to larger file sizes and slower loading and rendering times.

CSS approach

CSS uses a global namespace, which causes many problems at scale when many developers are building into the same web application. Since the 2010s, Meta has been writing CSS using an approach called CSS modules, which solves some of the problems with CSS at scale – global namespace, including styles only on pages that use them, dead code elimination (only including the selectors that are still being referenced), and some others. Meta's CSS modules implementation is not open sourced but bundlers like webpack add support for something similar.

In 2014, Christopher Chedeau gave an insightful talk about writing CSS within your JS. In 2018, Meta started their rewrite of facebook.com and announced a new way of writing CSS in JS, which is called StyleX. StyleX's API resembles CSS-in-JS libraries like Aphrodite and Glamor, and has these key features:

  • Generated atomic CSS stylesheet: An atomic CSS stylesheet has a logarithmic (flatter) growth curve because it's proportional to the number of unique style declarations rather than to the number of styles and features written. Hence the stylesheet does not grow as fast as the number of features and the file size will stay relatively similar even with thousands of features and pages. The cost is being paid by each page as the HTML size is now larger.
  • Type-safe: Property keys can be checked for typo mistakes as only a recognized set of keys are allowed (e.g. margin, padding, color, etc.). Components can decide which CSS properties they want to allow to be passed in as props (e.g. only margins allowed). This prevents users of your components from customizing in ways you don't want them to.
  • No runtime: The styles are extracted into a static CSS file at build time. Little/no JavaScript needs to be run to inject the styles. This is important to prevent Flash of Unstyled Content and instances where JavaScript is not executed or supported.
  • No specificity issues: Each atomic class declaration only contains one property and is not nested.
  • Determinism: With atomic classes, each class on a DOM element specifies a different underlying property, and you won't run into the issue of non-deterministic result which depends on the order of the classes in the stylesheet.

The team responsible for rebuilding facebook.com gave a talk about StyleX during React Conf 2019 and also at React Finland. The process of open sourcing StyleX has been a long one and as of Nov 2023, StyleX is finally open sourced. A caveat about StyleX is that it needs extra configuration to work in UI frameworks that use custom file formats like Vue and Svelte.

In the open source ecosystem, Tailwind CSS is the rage. Like StyleX, Tailwind also uses atomic CSS approach so they also share a similar benefit of a CSS stylesheet file that plateaus. However, since Tailwind CSS is just a set of atomic classes, you can use it within both JavaScript and HTML and even server-side Ruby templates. Out-of-the-box, the default Tailwind config provides a set of predefined classes (similar to design tokens) whereas StyleX does not.

Panda CSS is a new atomic CSS-in-JS library by the creators of Chakra UI and it is the middleground of StyleX (library calls are compiled away, type-safe) and Tailwind (inline style declarations, provides some default styling tokens).

The advent of React Server Components means that a whole category of styling libraries that relied on context for theming and runtime injection without any static stylesheet extraction will cease to work. Thankfully Tailwind and Panda CSS generate styles at build time and are fully-compatible with React Server Components.

If you don't mind seeing multiple classes in your HTML or building only with HTML and CSS (or non-JavaScript templating approach), Tailwind CSS will be my recommendation. If you prefer a JavaScript-first approach for styling and type-safety is important to you, use Panda CSS. Panda CSS is similar to StyleX but has a larger community maintaining it.

CSS preprocessing

Just like how developers can use the latest language features of JavaScript by using Babel, developers can also use the latest CSS features by using a CSS preprocessor like PostCSS to automate routine CSS operations, utilize future CSS syntax, and enhance CSS's functionality.

Using Tailwind and Panda removes the need for writing CSS directly, so you might not actually need to use PostCSS in your applications at all. By the way, Tailwind and Panda use PostCSS under-the-hood so you will still be using PostCSS, just not directly.

Linting

Because Meta uses StyleX for authoring styles, linting is done in JavaScript, which means ESLint and Flow. StyleX's type-safety features allow components to restrict styling props to be of only certain allowed properties (e.g. margin) and only allowed values (e.g. multiples of 4).

Teams using Tailwind CSS should install the official Tailwind CSS Intellisense plugin which offers autocomplete, linting and previews for a class' underlying styles.

If you're writing plain CSS or CSS modules, stylelint is the recommended linter.

Design tokens

Design tokens are essentially variables used to store design decisions such as colors, fonts, border radii, spacings, animations, and more. These tokens can be simple values like a number, color hex code, or more complex information like a typography scale (font size, line height, letter spacing).

By using design tokens, teams can ensure consistency across their product. As the design system evolves, changes made to the tokens automatically propagate throughout the product, ensuring uniformity and reducing the risk of inconsistencies.

On the web, design tokens can be implemented using CSS variables, or an object in JavaScript. Meta uses CSS variables for color tokens so that dark mode is essentially another set of color tokens. For spacing, border radii and other kinds of numerical values, tokens are not being exposed to developers because most design choices have been abstracted behind UI components built by the product's design system team.

In my opinion, UI components don't capture every design requirement and it's useful to make design tokens available for product engineers to use. Your company's product design team would probably already have selected design tokens and used them in their design system.

If you are using Tailwind or Panda CSS, the default configuration includes tokens for spacing, colors, typography, etc but they can be configured for custom design tokens. If you are not using Tailwind or Panda CSS and don't have a design system but want a similar default set of tokens, Open Props offers a good set of design tokens in the form of CSS variables.

Conclusion

An Atomic stylesheet is the recommended approach for styling on the web because the stylesheet size does not grow proportionately to the number of features/developers/products. Tailwind CSS and Panda CSS are modern libraries to achieve atomic stylesheet generation in a futureproof fashion that's compatible with React Server Components and a server-first component era.

Styling scalability checklist

  • Use Atomic CSS to keep the stylesheet size small.
  • Use CSS preprocessors to enable the latest CSS language features.
  • Use CSS linters to standardize on practices.
  • Use standardized design tokens.