Tailwind CSS class sorter – the custom way
Tags: tailwindcss, css, ruby, tooling
Published: 2021-09-21 14:46:17

Often, when a team adopts a utility-first CSS framework in a project, the folks gradually develop **a new superpower**: quickly visualize the utility-styled HTML snippets that they read. This helps a lot with rapid coding but doesn’t mean it’s always easy. Among the factors that affect this ability is **how reasonably and consistently are the utility classes ordered**.

Compare the following two divs (and we suppose here that you’ve already seen some Tailwind-styled code). Can you, same as us, read (and visualize) one of them quite easily while the other one seems harder to grasp?

```html
<div class="grid grid-cols-1 sm:grid-cols-2 sm:px-8 sm:py-12 md:py-16">

<div class="sm:px-8 md:py-16 sm:py-12 grid-cols-1 sm:grid-cols-2 grid">
```

While the two divs will render and behave precisely the same in the browser, they make quite a difference for our human minds. In other words, **order does matter**. Sure, it’s not as bad as if we _”swapped language our randomly in words common“_ but still, it slows you down when reading a template because your brain does not get immediate clues about the relative importance of the classes for the final look. 

## Our path to a custom script

At NejŘemeslníci, we’ve used utility styling since 2016 and several months ago we [migrated to Tailwind](https://dev.to/borama/series/11626). We’ve always had internal conventions for ordering utility classes but, not surprisingly, they were quite hard to obey consistently and they made the learning curve for our new team members apparently steeper. We needed to move on to an **automatic solution**.

There are quite a few good ones already but, as it turned out, we hit one blocker or another with each of them. Our biggest issue was that we use [Slim](http://slim-lang.com/) in our project, a template format which most of the tools don’t support.

- [Headwind](https://github.com/heybourn/headwind) is a nice opinionated sorter which probably [can be tweaked](https://github.com/heybourn/headwind#customisation) to understand our Slim templates but as a VS Code plugin it only works inside this particular IDE. We mostly use JetBrains RubyMine in our team and also needed a CLI version.
- [Tailwind Formatter](https://plugins.jetbrains.com/plugin/13376-tailwind-formatter/) is a nice clone of Headwind built for JetBrains IDEs but unfortunately it currently doesn’t support custom regexps.
- [RustyWind](https://github.com/avencera/rustywind) is a CLI for ordering Tailwind classes but also does not yet support custom regexps.
- **Update as of Jan 2022:** actually, there’s an official [Tailwind Prettier plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) available now! As much as we like and recommend it, it still does not help us because it doesn’t support Slim syntax either.

In short, we have not found a tool that would suffice in all our requirements. Luckily, it is **surprisingly easy** to build a custom sorting script!

## Meet Tailwind sorter

So we wrote a script plainly named **[Tailwind sorter](https://github.com/NejRemeslnici/tailwind-sorter)**. It is a ~110 lines standalone script written in ruby accompanied by a YAML config file. It is supposed to be customized, tweaked and **adapted for your specific needs** (and that’s why we don’t currently plan publishing it via ruby gems). It is **intentionally bare-bones but well customizable**.

Actually, we believe that a customized script is becoming **the proper way to go for Tailwind classes handling** today. All the tools that we’ve seen work with a huge static (sometimes configurable) set of classes ordered in some way.  However, with some of [the](https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0#user-content-shorthand-color-opacity-syntax) [recent](https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0#user-content-caret-color-utilities) [additions](https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0#user-content-per-side-border-color-utilities) to Tailwind, and especially with the [JIT mode](https://tailwindcss.com/docs/just-in-time-mode), the number of utility classes and their combinations skyrocketed and will continue to do so. A generic tool then would have to either expand its set of ordered classes accordingly or mimic some of the special Tailwind logic for matching and handling the classes dynamically.

We, instead, made a deliberately ”dumb“ but hackable tool that is set up to work best with only the real and unique set of utilities that you use in your project. Using the new [shorthand color opacity syntax](https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0#user-content-shorthand-color-opacity-syntax)? Add its handling to the regexp and/or script. Using [arbitrary values](https://github.com/tailwindlabs/tailwindcss/releases/tag/v2.2.0#user-content-extended-arbitrary-value-support)? You’ll probably have to amend the script as we don’t use them. Or, if you just happen to use one or two such ”dynamic“ classes, add them statically to the pre-configured set of ordered classes and you’re covered.

### Features

The script, [`tailwind_sorter.rb`](https://github.com/NejRemeslnici/tailwind-sorter/blob/main/bin/tailwind_sorter.rb), can operate in two distinct modes:

- it can edit a file in-place (which is suitable for integrating with various IDEs and editors or as a standalone CLI tool), or
- it can just warn about the wrong order of classes and leave the file alone (this is useful for integrating with pre-commit git hook systems, such as [Overcommit](https://github.com/sds/overcommit)).

The script loads the configuration from the [YAML file](https://github.com/NejRemeslnici/tailwind-sorter/blob/main/config/tailwind_sorter.yml), reads the given file line by line, uses the configured regular expressions to extract the set of classes from each line, sorts the classes using a configured order and writes them back, removing possible duplicates.

Out of the box, the utility classes are configured in several groups. If groups are used, the script orders classes with Tailwind variants (such as `hover:bg-black-100`) **near the end of the appropriate group**. Without groups the script sorts variant classes near the end of line.

By default, the script recognizes a few patterns in the Slim templates. But, using custom regular expressions, you can add support for almost any template file format you need.

Out of the box, the script will order **custom (unknown) classes first**. We are used to this pattern and believe it is more reasonable than ordering such classes elsewhere as, in our experience, custom classes usually bear a more important meaning than utilities and it also makes it easier to spot potential typos.

## Preparing the configuration

As we said, the script is supposed to work best with just the unique subset of utility classes that you use in your project. For that, we had to extract and pre-order them in the configuration.

We used the following process to do that in principle:

- we grabbed our production set of utilities from our main CSS bundle
- we also took the set of ~5000 ordered default classes from the [RustyWind project](https://github.com/avencera/rustywind/blob/master/src/defaults.rs)
- we reordered our classes according to the default order and manually moved the leftover ones to their appropriate places
- and finally, we grouped these custom-ordered classes and put them to the [config file](https://github.com/NejRemeslnici/tailwind-sorter/blob/main/config/tailwind_sorter.yml).

More details can be found in the [project README](https://github.com/NejRemeslnici/tailwind-sorter#adding-your-unique-set-of-tailwind-classes).

Once we’ve done this, we are ready to sort the utility classes in the most important places in our project. By the way, whenever we add a new class for the first time to the project, we spot this easily because it is re-ordered first on the line, and **we can add it to its proper place** in the Tailwind sorter config. However, since we are working on a mature project, this scenario is much less frequent than we would have thought before.

## Integration with your IDE

Since Tailwind sorter acts as a standalone command, it should be relatively easy to integrate it to the workflow that you’re used to. For example, we work in RubyMine most of the time. The IDE supports a [File Watchers](https://www.jetbrains.com/help/ruby/using-file-watchers.html) plugin that is able to run an arbitrary command upon saving a changed file. If you’re interested, details of this setup can be found in the [project wiki](https://github.com/NejRemeslnici/tailwind-sorter/wiki/RubyMine%2C-IntelliJ%2C-PHPStorm%2C-%E2%80%A6).

The workflow then is as follows: we edit a template file, write some Tailwind-styled code, hit Save and almost instantly the classes are reordered properly. Our front-end devs say this is **a huge time-saver** as they don’t have to think too much about the classes order themselves any more…

![Tailwind sorter in action in RubyMine](https://raw.githubusercontent.com/NejRemeslnici/tailwind-sorter/main/img/tailwind_sorter_intro.gif)

## Enforcing the proper order during commits

As a team we want to ensure that everybody commits our templates with classes rightly ordered. We use [Overcommit](https://github.com/sds/overcommit) to enforce consistency but any similar tool will do.

We set up a [pre-commit hook](https://github.com/NejRemeslnici/tailwind-sorter#guarding-sort-order-via-overcommit) that runs the Tailwind sorter script with the `-w` parameter, making it only warn about the ordering problems, without updating the committed file. (Overcommit [does not support](https://github.com/sds/overcommit/issues/238) changes to files during the pre-commit hook phase.) The script in this mode also shows the proper command to run to fix the problem:

```sh
~/my_project (main|✔) $ bin/tailwind_sorter.rb -w sample.html.slim
sample.html.slim:1:CSS classes are not sorted properly. Please run 'bin/tailwind_sorter.rb sample.html.slim'.
sample.html.slim:2:CSS classes are not sorted properly. Please run 'bin/tailwind_sorter.rb sample.html.slim'.
sample.html.slim:3:CSS classes are not sorted properly. Please run 'bin/tailwind_sorter.rb sample.html.slim'.
sample.html.slim:5:CSS classes are not sorted properly. Please run 'bin/tailwind_sorter.rb sample.html.slim'.
...
```

## Is this fast enough?

Ruby is not particularly known for being fast so this is a valid concern. We use a few speed optimizations in the script, such as the [`--disable-gems` option](https://github.com/NejRemeslnici/tailwind-sorter/blob/main/bin/tailwind_sorter.rb#L1) (we don’t need rubygems loaded as the script has no dependencies other than ruby itself and the stdlib). 

If you happen to call the script from a ruby environment governed by [Bundler](https://bundler.io/) and if your project is otherwise large and uses a lot of gems, it’s helpful to use the `Bundler.with_unbundled_env` wrapper (see our [tests](https://github.com/NejRemeslnici/tailwind-sorter/blob/main/spec/bin/tailwind_sorter_spec.rb#L17), for an example) as it will remove Bundler gems handling from the script itself, speeding up the start-up time considerably.

Other than that the script is bare-bones. Still, in our project, it is able to process around 30 template files per second which is fast enough for our needs.

Here are timings of running the script in the warning and in-place editing modes:

```sh
# warn-only mode:
~/my_project (main|✔) $ time bin/tailwind_sorter.rb -w sample.html.slim
sample.html.slim:1:CSS classes are not sorted properly. Please run 'bin/tailwind_sorter.rb sample.html.slim'.
...

________________________________________________________
Executed in   57,33 millis    fish           external
   usr time   42,32 millis    0,00 millis   42,32 millis
   sys time   14,14 millis    1,25 millis   12,88 millis

# in-place editing mode:
~/my_project (main|✔) $ time bin/tailwind_sorter.rb sample.html.slim

________________________________________________________
Executed in   54,90 millis    fish           external
   usr time   36,21 millis    0,00 millis   36,21 millis
   sys time   17,69 millis    1,26 millis   16,43 millis

```

## Summary

If we’ve got your attention, please read the [project README](https://github.com/NejRemeslnici/tailwind-sorter) and [wiki](https://github.com/NejRemeslnici/tailwind-sorter/wiki) and feel free to customize anything you’ll find there. And let us know how it worked for you. Good luck with Tailwind classes sorting! 🤞