Migrating Tachyons to Tailwind CSS (III – learnings)

borama

Matouš Borák

Posted on March 1, 2021

Migrating Tachyons to Tailwind CSS (III – learnings)

Hello for the third (and last) time. Having shown the tools to migrate, we can now discuss the issues we came across during the transition and what we learned from them.

Things to watch for carefully

We hit a few non-trivial issues that we had to figure out before continuing. Most of them were caused by our rules #1 and #2, i.e that we wanted the transition to be gradual with the two systems coexisting for some time. If we had a smaller and less critical website to convert, we’d probably just throw in the complete migration in a single shot and things would be much easier.

Tailwind position in the CSS source code

As Tailwind was supposed to become an equivalent replacement of Tachyons, the two systems had to coexist in the most similar way possible. For example, we had other custom styles defined besides Tachyons so the specificity of the styles came into play.

The specificity of all utility classes is generally the same, 0010, and as the CSS standard says, out of two styles with the same specificity, the one further down in the source code wins. Thus, the correct place to add Tailwind was right behind Tachyons. Such a setup ensured that:

  • the newly added (migrated) Tailwind classes were the ones that won over the Tachyons ones
  • Tailwind classes had the same specificity relationship to our custom CSS as Tachyons.

Relative position of Tachyons vs. Tailwind

Base styles

But just dropping Tailwind into the correct place would not work either. Tailwind comes with its own set of base styles (also known as ”preflight“ or ”normalize“ styles) that reset cross-browser inconsistencies.

The problem is that the Tailwind base styles behave a bit differently than the Tachyons ones and the two interplay in a complex way in certain situations. So we decided to just switch the Tailwind base styles off during the migration and bring them back once we finished converting all classes and turned off Tachyons.

We use build-time imports so the main Tailwind import file with base styles temporarily disabled looked like this:

/* tailwind.css */
/* 
TODO: uncomment once migration finished
@import "tailwindcss/base";
*/
@import "custom_tailwind_base.css";

@import "tailwindcss/utilities";
@import "custom_tailwind_utilities.css";

@import "tailwindcss/components";
@import "custom_tailwind_components.css";
Enter fullscreen mode Exit fullscreen mode

To some extent, this was a risky decision: we resorted to running Tailwind in a ”non-standard“ regime and swapping the base styles in the end might break many things. But actually this turned out to be mostly OK and we had to deal with only a few minor issues afterwards so we believe this choice paid off.

Migrate whole CSS properties per iteration

During the migrations, we quickly learned that we must convert all utility classes of a given CSS property in each iteration. Why? It’s related to specificity again and let’s quote MDN here:

Something to note here is that although we are thinking about selectors and the rules that are applied to the thing they select, it isn't the entire rule which is overwritten, only the properties which are the same.

Now, suppose we wanted to migrate the width scale but only the relative percentages scale (and that we expect to convert the absolute width scale later). An element on a page might then end up having these mixed width-related classes:

<div class="w-full w10-ns">
  <!-- ... `w-full` wins on desktops here! 😬 ... -->
</div>
Enter fullscreen mode Exit fullscreen mode

These classes try to say ”use full width on mobiles but only 3rems on larger devices“. While the w-full is an already converted Tailwind class, the latter class has not been migrated from Tachyons yet. Now, what happens on desktops? Note that both classes still have the same specificity (because media queries themselves do not affect specificity). Thus, on devices with both classes effective, the class declared later in the CSS source code always wins, in our case the full width from Tailwind.

The solution is to care to always migrate all utilities for a CSS property, e.g. if we’re converting widths, we must convert all classes dealing with the width property. And the same holds for all other properties.

Purge Tailwind CSS hard during migration

While tackling the last of the larger issues, we came to the conclusion that we needed a very tight control over what classes appear in the resulting Tailwind CSS file during migrations.

The biggest incentive for this were the colliding classes (classes that have the same name in both systems but different meanings and that are shown separately in the compare_classes.rb script output). If we left these classes in the output Tailwind CSS, they would immediately affect the styling. Instead, we had to ensure that each such class was included in the final CSS only after it was migrated.

For this reason, we applied purging in the Tailwind configuration, in such a way that only already migrated classes were allowed in the output CSS file. We leveraged the fact that the migration script generates a list of all classes migrated (see above), so it was easy to configure Tailwind in such a way:

// tailwind.config.js
module.exports = {
  // CSS purging during migration
  purge: {
    enabled: true,
    content: ["migrated_classes.txt"]   
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

One more detail: to keep the migration feedback loop fast, we wanted the Tailwind CSS file to get recompiled each time after we ran the migration script (migrated more classes) or otherwise updated the webpack configuration. For this, we added the migrated_classes.txt file as well as the PostCSS config file to the additional_paths section of the Webpacker configuration:

# config/webpacker.yml
default: &default
  ...

  additional_paths:
    - postcss.config.js
    - migrated_classes.txt
Enter fullscreen mode Exit fullscreen mode

The additional_paths array can be used to add more filenames or globs to the ones automatically watched by Webpacker. If any of these files changes, the Webpacker assets recompilation is automatically triggered upon the next page load. The default files can be found in the default_watched_paths method of the Webpacker gem.

Finally, we intentionally left migrating the colliding classes to the very end of the whole migration process, so that we don’t affect the rest of the team more than absolutely needed.

Smaller issues

Some class names in pure CSS must be escaped

Some of the Tailwind classes use special characters (e.g. slash such as in w-1/2) that need to be escaped when in the context of CSS. You probably should not have to deal with this though as these classes are OK to write unescaped in templates / partials / helper functions and there is usually no reason to use them in a pure CSS context. It’s rather only the migration scripts that need to be aware of this detail.

Some classes generate a lot of false positives when migrated

Migrating classes can be particularly demanding when the same strings are used in a lot of places throughout the view layer but with other meanings than class names. For example, our Tachyons CSS included the "h1", "h2", "b" or "i" classes, the names of which collided with HTML element names in the templates.

The main regexp in the migration script tries to carefully match only actual class names but it was inevitable that we met a few cases that generated many unwanted replacements due to false positives. We had to go through all changes and correct them manually and then we added these ”special cases of collisions“ to the already_migrated section of the main config file so that the script does not try to migrate them ever again.

Here is a rather funny little example of such an unexpected collision in the width scale that we saw in one of our old layout files and had to deal with manually:

Collision of w3

Another surprising case was the .times utility which switches on a serif font in Tachyons and which collided with the #times method in our Slim templates. The class was indeed used sporadically in the codebase so we had to tackle it manually.

The scripts ignore classes defined only in responsive variants

Curiously, we found one or two Tachyons classes (that we manually added long time ago) which were declared only in one of the responsive variants but not in the corresponding base form. The classes comparison script ignores these cases so we had to identify and deal with these cases manually. Our migration process simply didn’t support such scenario and we think it is OK as we rather consider this a bug in the styles definition.

Particular differences between Tachyons and Tailwind

Apart from the rather technical issues above, there are also important differences in the way the two systems are designed and how they approach page styling in general. Sure, the core concepts are basically the same. But the two systems often emphasize different aspects of styling and this fact must be taken into account when migrating. We already mentioned the base styles above but there is more.

The scales

The default scales differ considerably in both systems. In Tachyons, most scales are constructed around powers of two. When adopting Tachyons years ago, we found this scale system to be nicely mathematically consistent but a bit too coarse for our needs so we added more points in between the default ones.

In default Tailwind, the scales are more detailed and pragmatic but sometimes less consistent: e.g. the max-width scale uses named values (”sm“, ”md“, …, sadly even different than default responsive breakpoint names) instead of numbers whereas the max-height scale is the same numerical scale as most others.

Default Tachyons vs. Tailwind scales

Overall, when migrating, we found out that our (previously amended) Tachyons scales were mostly the same as the Tailwind numerical scales and the two mapped nicely. We redefined the max-width, min-width and min-height scales in Tailwind to make the fit perfect.

By the way, it’s nice that adding (or completely redefining) the scale system is so easy in Tailwind. On the other hand, care must be taken that modifying the defaults is not overused. In the end, we added an Overcommit rule banning further updates of the Tailwind configuration (of course, this can be temporarily disabled, when truly needed).

Font sizes and line heights

Tailwind 2.0 changes the meaning of font size utilitiesthey now set the line height (”leading“) as well. Sadly, this broke too many places on our site so, as advised, we redefined the font size scale to include only font sizes again. And we left this small issue to a future refactoring.

Borders

Borders are an example of how differently the two systems approach a styling feature. For a border to appear around an element, you need to set the border-style and border-width properties. Tachyons sets both of them (solid 1px border) in its basic border utilities "ba", "br", etc… If you need a different width, for example, you need to effectively override it by using both utilities together, such as: "ba bw2".

On the other hand, Tailwind border utilities cooperate closely with the base styles. All elements are ”normalized“ to have a solid 0 px border by default. Thus, you usually only need to set the border width via a single utility class, e.g. border-r-2.

To sum this up, we essentially needed to replace the Tachyons utility couples to a single Tailwind class. We did this by search-and-replace in multiple iterations, continuously checking if all still looks good.

Finishing tasks

All right! This is about everything we had to do to get our website to a working Tailwind CSS. When we finished the rudimentary migration work, we did some finishing touches.

Set production-ready purging

During migration, we purged the output CSS to explicitly contain only the migrated classes. This is not needed any more, so we can edit the Tailwind config to reflect a more real-world purging setup.
Be sure to update the globs to match those files where you have Tailwind utilities in your codebase:

// tailwind.config.js
module.exports = {
  // CSS purging ready for production
  purge: {
    enabled: process.env.NODE_ENV !== "development",
    // uncomment to test purging locally
    // enabled: true,
    // Update also the production section in webpacker.yml when changing this.
    content: ["app/views/**/*", "app/helpers/**/*.rb", "app/javascript/src/**/*.js", "app/presenters/**/*.rb"],
    options: {
      blocklist: []
    }
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

We recommend to accompany this configuration with the following Webpacker setup:

# config/webpacker.yml
production: &production
  <<: *default

  # Trigger recompilation on each change of all templates (and other files watched by purgeCSS).
  # This ensures that a class from Tailwind used for the 1st time in a view template will not be purged.
  # The globs must be consistent with those in tailwind.config.js.
  additional_paths:
    - postcss.config.js
    - app/views/**/*
    - app/helpers/**/*.rb
    - app/javascript/src/**/*.js
    - app/presenters/**/*.rb
Enter fullscreen mode Exit fullscreen mode

Everything is already said in the comment but basically this ensures that the CSS pack files will get recompiled (and re-purged) on production when a template changes since the previous deploy. Thus, when you use a Tailwind utility class for the first time in one of these files, it will trigger the recompilation process and the class will not get purged.

Swap the base styles

If you remember, we commented out the Tailwind base styles during the migration process. Now is the best time to swap them, i.e. to uncomment the Tailwind base styles and comment out the Tachyons ones. Chances are high that this switch will break a few things on your web. We had to manually edit a few places so that the code fitted the Tailwind defaults instead.

Get rid of Tachyons and start styling with Tailwind!

Once we had the web up and running on Tailwind classes, we simply removed Tachyons. We told the whole team to only style with Tailwind from now on and celebrated this hard endeavor that we just finished! 🎉

Note on Rubymine support

Most of our devs use Rubymine for developing. It turns out that Rubymine support for Tailwind CSS is both good and mediocre at the same time.

If you use only the basic Rails stuff for your front-end, i.e. ERB templates, you are fine and you can leverage the great-looking auto-suggestions for Tailwind classes.

However, if you use a different templating language, the support is usually missing completely. We use Slim for our templates and there are two issues with this currently in Rubymine:

  • Rubymine marks some of the Tailwind classes as errors in Slim
  • the auto-suggestions don’t work at all in Slim templates.

”Errors“ in a Slim template in Rubymine

Luckily none of these issues affect how the templates themselves work in Rails, they just clash with presenting and working with them in Rubymine. And Slim itself already supports all special characters that Tailwind uses nicely.

Please vote for this issue if you need the Tailwind support under Slim templates in Rubymine, too!

What we learned

We came to a few learnings, some surprising, some less so… Here are the main ones:

Experience needed

As migrating between the two design systems is essentially nothing but a full-text search and replace, you should be well versed in this task. Knowing the possibilities of your text editor / IDE is essential and a good knowledge of regular expressions helps tremendously. Keeping and documenting all your changes in a version control system is a must.

A second know-how that is hard to do without during migrations is the CSS peculiarities, especially the cascade, inheritance and specificity. Without getting a deep-enough knowledge of these topics, you’ll often end up with the migration iteration outputs making no sense. Especially if, like us, you used a utility based design system for long enough, chances are that you’ve never actually needed to get to know these concepts too well. If that is the case, do it now or you’ll hit the wall very early during migration.

Overall, we actually think it is very liberating that all you need to migrate a site is text processing, constrained with a few rules from the CSS specs. Again, that’s all! As we tried for ourselves, it definitely is possible to convert a fairly large website with years of code piled up in the view layer that uses several different approaches to front-end development…

Attitude needed

Migrating the utility CSS of a website is a task that consists of many many tiny replacements all over the codebase. As such, the developers that migrate must be incredibly focused and patient throughout the process. For example, in our web, we replaced around 650 distinct classes in more than 30,000 places in around 800 files. Each of these replacements affects the visual style of a single place on the website and each of them, potentially, could lead to a visual bug.

Moreover, a migration like this is a refactoring task and as such it requires a certain approach towards code changes. We believe that the most helpful way of doing this is making tiny changes to the code, one at a time, continuously checking and deploying them, ensuring that the web as a whole is always in a functional production state.

Finally, when doing the last finishing steps described above, it helps to not treat bugs you find as bugs of the page styling but as bugs of the migration process instead. So, what you need, is not to fix the styling in the given spot on the page but to go deeper and find out why the styles behave like they do now and what exactly got wrong in the migration process. By doing this, we retroactively found a few more issues that needed to be fixed globally instead of reacting only to the particular pages that we caught as visually wrong.

Technical aids

It surely helps a lot to have a good coverage with tests, especially system tests. Many times we broke the system tests after we migrated a certain utility, because we missed some spot in the codebase or the migration script didn’t function well (we tuned it to be almost perfect though) or the Tailwind classes did not fit perfectly to the Tachyons ones. Quite often, the browser in the system tests tried to click on an element that was hidden, overlapped, or renamed while the corresponding JavaScript code was not updated. So, be sure to have at least the most important use cases covered by system tests before you try the migration.

We briefly considered adding visual regression tests to the migration pipeline. These tests should be able to catch all visual differences between the two versions and, in theory, might help tremendously. In the end though, we are happy we did NOT set out this path as, in general, the migration is surely not pixel-perfect and there were small (few-pixel) differences all over the web and during the whole process. We estimate that managing visual tests would actually have a rather negative impact on the effectiveness of the transition.

Also, it definitely helps to clean up the code as much as possible prior to the migration. We actually spent nearly two weeks cleaning up our code (and other preparation works).

Summary

If you ever set out on the journey of replacing your Tachyons CSS with Tailwind, good luck, it is definitely not an easy task, but entirely doable. And drop us a note if you will!

Should you want to read more about technical aspects of a Rails web development, please follow us here or on Twitter.

💖 💪 🙅 🚩
borama
Matouš Borák

Posted on March 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related