Category:Algolia

Fragments CSS System

A customizable utility-first CSS system developed for the Algolia web team.

Author

10 min. read

Post image
Fragments CSS System

Introduction

In 2018, I joined the website team at Algolia and had the pleasure of maintaining the marketing website, including our CSS solution. The website itself was built using Middleman and SCSS at the time, which worked well but its slow auto-reloading exacerbated the frustration of dealing with numerous CSS files to style our pages and components. Clearly the "system" we had in place was not good enough.

Something had to change, so we came up with a solution - We wanted to build a system that would fulfill a few needs:

  • It had to be easy to build, use and maintain.
  • It had to be performant, with a small footprint. (Bundle size)
  • It had to be configurable out of the box to fit some of our different needs for various projects.

So we got to work.

How

SCSS and mixins were heavily utilized in the creation of the initial version, resulting in a working prototype in a rather short time. Believing this to be a promising direction, we expanded upon it to generate all the CSS we thought we needed and immediately put it to use in production. ⚡️

A few months went by and things were going well, but I wasn't entirely satisfied anymore. The initial excitement had worn off and I became frustrated with the lengthy build times caused by the use of SCSS. In an effort to improve performance, I decided to try rewriting the project in Node.js. After just a couple of hours, I had a working prototype and the results were impressive. Within the span of a weekend, I had the new version at feature parity with the SCSS version and I presented the project to the team, after which the prior version was quickly deprecated in favor of the new one, which was not only faster but also meant one less technology for us to use. (JS all the things! 💪)

Command Line Interface

For the project, I built a simple CLI using Commander and Ora, which handles the following commands:

  • fragments init: Initializes a new config file in the root of your project.
  • fragments build [path]: Builds the CSS file to a ./dist folder or to the path param, if present.

Nothing fancy here but it does the job it's supposed to do, which is all you can really ask for. Maybe in the future we can add additional functionality if we find the need.

Commander and node:fs is doing the heavy lifting here, Ora only really being used in order to show a small spinner in the console while the output is being generated.

Class Generation

I won't be showing the primary generator itself because it's quite the mess, not to mention the question about whether or not I'm allowed to. But I can show some examples of what a module definition in Fragments looks like:

export const backgroundColor = ({ included = true, responsive = false, hover = false, classes }) => {
  if (!included) return '';
 
  const combinedClasses = {
    transparent: 'transparent',
    current: 'currentColor',
    ...classes,
  };
 
  return classNames(combinedClasses, 'bgc', 'background-color', responsive, hover);
};

Most modules follow a pattern like the ones shown here and as you can see, most modules are fairly straight forward with just a couple of parameters being passed to tweak the class generation. CSS variable generation is a bit different of course but nothing mind blowing or extremely complicated at all.

The classNames function is just a small helper which makes it easy to generate the desired output for the vast majority of cases.

  1. In the case of background-colors, the module also contains a few predefined classes to generate as sensible defaults.
  2. margin returns an array instead, which allows us to generate additional utilities for setting margins on each axis.
  3. Setting CSS variables is a bit of a mess though and only supports colours for the time being…

All of this is of course something we can improve on and even extend as needed in the future as the project evolves.

Configuration & Output

As you will see here, our configuration file is pretty simple albeit quite verbose when compared to the one in Tailwind for example. We could improved this I'm sure but ultimately we had something which worked and our time was better spent elsewhere.

// Config object where you define rules and colours etc. for
// the underlying systems to use
const config = {
  // General settings for the compiled code
  options: {
    namespace: 'frg', // Think of this as a prefix
    separator: '\\:', // The separator used between classname and the variant
    important: false, // Forces all classes to be marked important
  },
  // Define your breakpoints
  breakpoints: {
    sm: '768px',
    md: '960px',
    lg: '1200px',
  },
  // Define your brand colors and any others you might want
  // to reuse across your project. Values like`transparent`
  // and `currentColor` are provided by Fragments without
  // needing to define them here yourself
  colors: {
    // Single values
    white: '#fff',
    black: '#000',
    // Array of values
    neptune: ['#3944a0', '#565db6', '#7178cc', '#8c93e2', '#a6b0f9'],
    // Object of values
    mars: {
      100: '#ed5a6a',
      200: '#f27885',
      300: '#f695a0',
      400: '#fbb3ba',
      500: '#ffd0d5',
    },
  },
  // Spacing is used mainly for paddings and margins
  spacing: {
    0: 0,
    8: '8px',
    12: '12px',
  },
  // Sizes are for widths and heights mostly
  sizes: {
    0: 0,
    10: '10px',
    20: '20px',
    '10p': '10%',
    '20p': '20%',
  },
};
 
// The actual config where you will define modules and
// their specific rules and classes
module.exports = {
  config,
  modules: {
    root: {
      // Exposes the colors as CSS variables for external use
      cssVariables: {
        classes: config.colors,
      },
    },
    backgrounds: {
      background: {
        responsive: true,
      },
      backgroundColor: {
        responsive: true,
        hover: true,
        classes: config.colors,
      },
    },
    // And so on for all the other modules…
  },
};

We could have been more strategic in the way we named our generated classes - most of the time, we just shortened the CSS attribute name without too much thought and this has caused conflicting names on more than one occasion. Additionally, we constantly had to make pull requests to add attributes that we either forgot about or realized would be useful during projects.

Outcome

Ultimately, we achieved most of the things we wanted with Fragments. It's fast, it's customizable and it's not impacting our bundle size beyond the initial set of generated classes.

The only thing we didn't get around to adding, was making the tool extendable with custom plugins. In hindsight, it wouldn't be too difficult to add but considering that it's an internal tool where we control everything anyway, we didn't find the need yet.

Since launching Fragments internally, I've left the team whom Fragments was primarily built for, but I still retain technical ownership over the project and was pleasantly surprised to see a pull request 4 years after its inception, to replace Babel with SWC for compiling, which improves the build times even more! Amazing~ ⚡️

TechnologyBuild timeDeveloped
SCSS~12 secondsApril 2018
Babel~1.9 secondsMay 2018
SWC~0.5 secondsApril 2022

Learnings

Building something from scratch is easy. Ensuring that it's great means spending the time doing research on which technologies are the best to use and, more importantly, if something similar already exists.

In our case, Tailwind had just appeared on the scene shortly after we finished the first version of Fragments and instead of continuing on this, we should have migrated over instead immediately. However, we kept maintaining Fragments for about 4 years before finally beginning the migration.

That said, we did learn a lot from this project and it allowed us to develop our other projects much faster than we had previously, and Fragments.js remains in production as of time of writing.

Final Words

Starting this project, I didn't know how to even approach writing a custom CSS generator like this, but in the end it was a lot simpler than I could have imagined. It really helps to break down the problem as much as possible - It's just files and functions. node:fs handles files, and with functions you can do whatever you wish, it's really not complicated, it just takes a bit of time.

It's a fun learning experience and something I can recommend you try doing yourself. Good luck and have fun! I would have liked to be able to share the repository here, however the project is still private as of time of writing. 😔

Shout-out to

Ronan Levesque and Lucas Bonomi whom I worked with on the original SCSS version.

And if you want to read more about the benefits of utility-first CSS, check out the article "In Defense of Utility-First CSS" by Sarah Dayan.

Since working on Fragments.js, I've started work on something similar in my spare time, but with an expanded set of features and improvements such as:

  • Written in TypeScript
  • Has great test coverage
  • It's extendable through custom plugins
  • Far more properties supported out of the box, including P3 colour gamut support
  • More flexibility in regards to the config
  • Making use of LightingCSS instead of PostCSS

Stay tuned for more on this!

Inspiration