October 2020 was one of the most exciting months for the Frontend team at Ebury because we officially stopped supporting Internet Explorer 11. This allowed us to remove lots of polyfills and optimisations required only by the IE browser and it also enabled features and technologies that were unavailable due to lack of support. We started shipping native ES6+ code, removed performance optimizations for HTTP/1, and fully utilized HTTP/2, but most importantly, we started using CSS custom properties (A.K.A. CSS variables). This led to a significant overhaul of our CSS implementation as we decided to deprecate SASS and adopt PostCSS instead.

Life with SASS

The SASS language was already very well established in our design system mainly because of the benefits like:

SASS variables: Reusable values to avoid repetition in the stylesheets and do maths easily.

Nesting CSS rules: To compose rules and maintain their naming consistency. Very useful regardless of whether you use any methodology/paradigm e.g. SMACSS, BEM, or OOCSS.

Theming: Solutions at Ebury are fully themeable, and we support more than 230+ themes at the moment. Being able to generate a theme and its color palette just by changing a base color variable is an essential requirement we have to deal with.

SASS functions and modules: SASS language contains lots of useful helper functions and modules for manipulating colors e.g. darken, lighten, saturate, grayscale. Our design system uses 25+ colors and all of them are calculated from a single base color of each theme. These functions play an important role in the implementation.

Generating classes: We use utility classes, not everywhere, but they are part of our design system and we are thankful we could generate them instead of writing them from scratch.

Life after SASS

Let’s have a look at a different approach using CSS custom properties and PostCSS.

Variables using CSS custom properties

The major advantage of CSS variables is that they are declarative. If one variable is re-used by the second variable, each time the first variable changes it will automatically update the calculated value in the second variable too. Meanwhile, the SASS variables are imperative so the change in one variable is not synced into other variables and the change propagation must be done by the developer in the code.

On top of that, CSS variables can get changed during the runtime (either via JavaScript or via CSS), but SASS variables get resolved during the build process and are no longer present when the generated CSS runs in the browser.

Just like in SASS, you can do maths with CSS variables too using the calc() function. It is also declarative so it has the same advantage over SASS and because the expressions evaluate in the browser during the runtime you can also use relative units like em or percentage.

body {
  width: calc(100% - 2 * var(--body-padding));
}

Nesting CSS rules with PostCSS

Native CSS nesting is still in draft, but it is possible to use the PostCSS plugin to simulate the same behaviour as developers are used to in SASS. There is also another plugin available that matches the draft closely so it is possible to make your CSS ready for future implementation.

Theming

While building the applications at Ebury, we need to generate 230+ themes. That means the same SASS files have to be built 230 times with different theme colors. With CSS variables, we only need to build the CSS bundle once and let the browser apply values when running the application.

This has a significant impact on build times (it went down from 20 minutes to 20 seconds) and also the application performance due to better caching. The application now has to serve only one CSS file to all clients regardless of the theme they use. This means we serve fewer files and the files are more likely to be already cached in CDN when serving them to the users with a cold client cache.

It is also easier to deliver new themes because we can test a new theme directly in the browser without additional builds/deployments. 

HSL to replace SASS functions and modules

If you are using the SASS color module to manipulate colors, I have good news for you. The CSS already supports those in the form of HSL notation. A code like this:

body {
  $main-color: #00bef0;
  background-color: lighten($main-color, 10);
  color: saturate($main-color, 10);
  border-color: grayscale($main-color);
}

… can be written using plain CSS and HSL like this:

body {
  /* #00bef0 equals hsl(193deg, 100%, 47%) */
  --main-color-h: 193;
  --main-color-s: 100%;
  --main-color-l: 47%;
  background-color: hsl(var(--main-color-h) var(--main-color-s) calc(var(--main-color-l) - 10%));
  color: hsl(var(--main-color-h) calc(var(--main-color-s) - 10%) var(--main-color-l));
  border-color: hsl(var(--main-color-h) 0% var(--main-color-l));
}

Hsl() and hsla() notations have been supported since IE9. You don’t need SASS for doing this.

Generating utility classes with TailwindCSS

One of the best features of SASS is the ability to generate the CSS classes using @for and @each  rule. You may find code generating utilities for padding like this in your codebase too:

@for $px from 1 to 8 {
    $size: $px * 4;
    
    .p-#{$size} {
        padding: $size;
    }
    
    @each $pos in top, right, bottom, left {
        .p#{$pos}-#{$size} {
            padding-#{$pos}: $size;
        }
    }
}

The problem with this approach is that you must write and maintain this code on your own. And the number of utility classes needed by your team grows over time. You may have started with classes for paddings and margins, but later expanded their support with negative values, then you also added colors and started generating utility classes for background colors, text colors. borders, flexbox, grids, text positions following in the next iteration. Well, there is a better way to generate and maintain those with PostCSS and TailwindCSS.

TailwindCSS is capable of generating utility classes for every CSS property with only a simple configuration file. You specify the spacing you agreed with your design team on and TailwindCSS generates thousands of useful utility classes based on the configuration. Check out the following spacing:

spacing: {
  0: '0',
  4: '4px',
  8: '8px',
  12: '12px',
  16: '16px',
  20: '20px',
}

TailwindCSS should automatically create utilities for padding, margin, height, max-height, min-height, width,  min-width, max-width including the negative values. Just changing one line of code you can enable a responsive variant of these utility classes and they are ready for use in your HTML:

<div class="-m-8 p-8 md:p-24 h-full min-h-72 w-full bg-green focus:bg-red">...</div>

You can add a custom colors configuration and create thousands of classes for background, borders, fill, stroke and text, and thanks to the CSS variables, you can make all of them themeable, just like this:

colors: {
  theme: {
    1: 'hsl(var(--main-color-h), var(--main-color-s), 10%)',
    2: 'hsl(var(--main-color-h), var(--main-color-s), 20%)',
    3: 'hsl(var(--main-color-h), var(--main-color-s), 35%)',
    ...
  },
  gray: {
    1: 'hsl(var(--main-color-h), 10%, 10%)',
    2: 'hsl(var(--main-color-h), 10%, 20%)',
    3: 'hsl(var(--main-color-h), 10%, 35%)',
    ...
  },
  error: {
    DEFAULT: 'hsl(var(--color-error-h), var(--color-error-s), 50%)',
    dark: 'hsl(var(--color-error-h), var(--color-error-s), 35%)',
    light: 'hsl(var(--color-error-h), var(--color-error-s), 65%)',
  },
},

But that’s not the only benefit of this approach we noticed after adopting the TailwindCSS. We have finally reached proper consistency in our CSS. We agreed, as a team, that we should use in our CSS files and components only classes generated by the TailwindCSS. This means that anything outside of our config is outlawed. Our design team specified that the padding should always be a multiple of 4 so we configured that and now there is no way for developers to accidentally set a padding of 5px or 10px. We’ve done the same with colors, line-heights, fonts, box shadows, and z-indices. If a developer wants to make an exception to these rules, they have to use plain CSS or a style attribute and then defend that during the PR. For reviewers, it is easy to spot these exceptions too and open a debate why that exception is needed.

The code readability suffers at the beginning while everybody adjusts to the different way of writing CSS but it becomes natural after a month or two. There are also arguments about using the utility-first approach is bad and makes the code messy and we agreed with them after using it for a while as we discovered some components are harder to understand and debug. For example, this is our button component which contains lots of functionality using only utility classes:

<button class="w-full py-8 px-24 outline-none btn-text text-gray-8 bg-gray-4 fill-current text-center no-underline inline-flex align-bottom flex-row items-center justify-center cursor-pointer border border-solid border-transparent whitespace-nowrap overflow-hidden transition-colors duration-150 ease-out">...</button>

That is lots of classes and we quickly abandoned this approach because even if we get used to the TailwindCSS class names, we still struggled to understand what is the purpose of these classes. BEM methodology worked for us well in the past so we decided to return back to it and combine BEM with TailwindCSS like this:

.btn {
  @apply inline-flex align-bottom flex-row items-center justify-center;
  @apply bg-gray-4 btn-text text-gray-8 fill-current text-center no-underline;
  @apply cursor-pointer;
  @apply py-4 px-16;
  @apply outline-none border border-solid border-transparent;
  @apply whitespace-nowrap overflow-hidden;
  @apply transition-colors duration-150 ease-out;

  &--full-width {
    @apply w-full;
  }

  &--lg {
    @apply py-8 px-24;
  }
}

And the HTML looks cleaner:

<button class="btn btn--lg btn--full-width">...</button>

The result is easier to understand and debug because it is more important to know that the generated button is at full width and large size than knowing the button has classes w-full py-8 px-24. The context provided by BEM and its modifiers turns out to be more valuable to us.

You may also notice that this approach enables the grouping of properties because the @apply directive accepts more than one TailwindCSS class. Another huge win for the readability of the code. You can see the classes for flex, colors, transitions, borders, and padding nicely grouped together. After trying this for a while we decided to keep BEM with @apply directive for all reusable components as their CSS can grow significantly and use the utility classes in HTML templates only in container components for their layout/grid, or spacing between reusable components, or any kind of deviation/exception in the design that is not reusable.

Wrapping it up

We’ve been working with CSS variables and TailwindCSS for a year now and we enjoy it a lot. We have seen lots of performance gains in build and run time too. The code is easier to maintain and read. Lots of inconsistencies in our design that slipped through PRs get identified and eliminated.

If you’d like to see all of it together in action, check out our components library on GitHub.

Join the team changing the future of FinTech

Apply now!