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?
<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.
At NejŘemeslníci, we’ve used utility styling since 2016 and several months ago we migrated to Tailwind. 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 in our project, a template format which most of the tools don’t support.
- Headwind is a nice opinionated sorter which probably can be tweaked 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 is a nice clone of Headwind built for JetBrains IDEs but unfortunately it currently doesn’t support custom regexps.
- RustyWind is a CLI for ordering Tailwind classes but also does not yet support custom regexps.
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!
So we wrote a script plainly named 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 recent additions to Tailwind, and especially with the JIT 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? Add its handling to the regexp and/or script. Using arbitrary values? 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.
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).
The script loads the configuration from the YAML file, 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.
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
- 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.
More details can be found in the project README.
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.
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 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.
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…
As a team we want to ensure that everybody commits our templates with classes rightly ordered. We use Overcommit to enforce consistency but any similar tool will do.
We set up a pre-commit hook 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 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:
~/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'. ...
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 (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 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, 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:
# 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
If we’ve got your attention, please read the project README and 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! 🤞