Skip to main content

By Rachel Opperman

How to Incrementally Upgrade Tailwind CSS Versions to Avoid Breaking Changes

At Zaengle, we were very early adopters of Tailwind CSS.

As a result, we ended up with several codebases that were using Tailwind version 0.7.

At the time of writing, the latest version is 3.2, and there are quite a few breaking changes between those versions.

The Tailwind docs provide upgrade guides for upgrading from v0.x to v1, v1 to v2, and v2 to v3. But if you need to make a big jump, like we did, and go from v0.x to v3, then the migration guides won't work for you. It's not as simple as updating the package version and changing a few config values and classes. In fact, if you try to update the package version, you'll find that your code won't compile.

If you're updating a small codebase, this may not be a big deal because you likely won't have a ton of changes to make. But if you're working in a large codebase, it simply won't be feasible to make every update necessary just to get the code to compile. This would prevent you from making granular updates and checking that your styles still look the same as you update them.

So, is it possible to have two distinctly different versions of Tailwind CSS existing in the same codebase without conflicting with one another?

As it turns out, it is.

This possibility is one of the reasons we decided not to use Tailwind Shifts. We preferred to migrate directly from v0.7 to v3. We also wanted to be able to make updates in chunks, rather than updating the entire codebase in one go.

Having multiple versions of Tailwind in your project at one time allows for progressively updating parts of the codebase without affecting the rest of it. This was a big win for us because it allowed us to deploy updates while keeping the rest of the site in working order.

For example, while we were updating the Vue components in a Craft CMS site from Vue 2 to Vue 3, we were able to update those components from Tailwind v0.7 to v3. We could then deploy the Vue changes without having to update the rest of the Tailwind in the codebase first. This allowed us to get valuable JavaScript updates into the production site much more quickly than would have been possible if we couldn't have two Tailwind versions coexisting with one another.

In this post, we'll detail the process we devised for incrementally upgrading from an outdated Tailwind version to the latest version while avoiding breaking changes and broken builds.

Spoiler alert: This is going to be quite the journey, so place your seat backs in the upright position and buckle up!

Also, please note that the process detailed below is based on the dependencies available as of February 2023, so it may require some adjustments for your situation.

Installing the necessary dependencies

There are three initial steps to take in this upgrade process:

  1. Create a src directory for containing the latest Tailwind code
  2. Initialize a package.json file in the src directory
  3. Install the latest Tailwind dependencies, plus a couple others needed for production builds

Create the src directory

The first step is to create a new directory that will contain the separate dependencies, scripts, and configuration necessary for using the latest version of Tailwind. In the root of your codebase, create a src folder.

Note: The folder can be named something else if you'd prefer; src is our convention, so that's what you'll see in this post.

Initialize a package.json file and install dependencies

Next, from within the src directory, run npm init -y or yarn init -y to create a base package.json file. You don't need to do anything else with this file for the moment.

Now we can install the dependencies that we'll need. Our package manager of choice is Yarn, but feel free to use whichever package manager you'd prefer.

To install the base packages we'll need, run the following from within the src directory:

$ yarn add -D tailwindcss postcss autoprefixer

We'll also need the PostCSS CLI and cssnano for production builds, so run the following from within the src directory:

$ yarn add -D postcss-cli cssnano

Now that we have all of the necessary dependencies installed, let's move on to adding the configuration that we'll need.

Adding configuration

There are two config files that we'll need – tailwind.config.js and postcss.config.js. To generate these files, run the following from with the src directory:

$ npx tailwindcss init

Once these files have been generated, they'll need to be updated.

Update the PostCSS configuration

There are three plugins that need to be added to the postcss.config.js file.

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    cssnano: {
      preset: 'default',

    },
  },
}

The tailwindcss and autoprefixer plugins are added as directed in the Tailwind installation with PostCSS docs.

The cssnano plugin is used to optimize the CSS created for a production build. We're using the default prefix here, but this is something that you can customize to your needs. The cssnano presets docs provide more information.

Update the Tailwind configuration

In the tailwind.config.js file, update the content array to include the paths to your template files. This will vary depending upon your project, but here's what it might look like in a Craft CMS project that utilizes Vue:

content: [
  '../templates/**/*.twig',
  '../resources/js/components/**/*.vue',
]

The other key thing that needs to be updated is the prefix. This is what will distinguish the classes for the new version from the classes for the older version. Add a key-value pair that looks something like this:

prefix: 'hc-'

Important note: Choose your prefix carefully to make things easier on yourself in the long run. You'll want something that doesn't already exist in Tailwind (e.g., z-, p-, m-). You'll also want something that doesn't show up elsewhere in a Tailwind class. For example, I made the mistake of choosing g- for one project because I didn't consider that g- could exist elsewhere (e.g., in Tailwind's leading- classes). This prevented me from doing a simple find and replace to remove the prefix when the upgrade process was complete.

Any other custom configuration that you need can be added by pulling it from the config file for the old version (the tailwind.js file if the project is currently using v0.x). You can reference the Tailwind theme configuration docs if you're unfamiliar with configuring newer versions of Tailwind.

Adding scripts

Now that we have everything installed and configured, we need to add some scripts that we can run to watch and/or build the new CSS.

Update the scripts section in src/package.json to the following:

"scripts": {
  "watch": "npx tailwindcss -i ./tailwind-3.css -o ../resources/scss/v3.css --watch",
  "build": "npx postcss ./tailwind-3.css > ../resources/scss/v3.css"
}

Let's examine these two scripts in more detail.

The watch script

The watch script is what you'll need to run when you're making updates in your local development environment. Here's an explanation of the parts of the script.

npx tailwindcss will run the Tailwind CLI.

-i ./tailwind-3.css denotes the input file. You can use another filename if you'd like, but make sure you create this file in the src directory.

-o ../resources/scss/v3.css denotes the output file. This is just an example, so be sure to choose the directory that makes sense for your project. You can also choose a different filename. This file is compiled, so you'll want to add it to your .gitignore file.

--watch runs the command in "watch" mode, meaning that it will keep track of changes in the tailwind-3.css file and recompile the v3.css file as changes are made.

The build script

The build script is what needs to be run as part of a script in the root package.json file. It will build an optimized CSS file that can be included as part of a production build.

It takes the new CSS (the tailwind-3.css file) and places an optimized output file in the specified directory with the specified filename (the root resources/scss/v3.css path, in this case).

The root build:tw3 script

Now, let's examine a script we can place in the root package.json file.

"scripts": {
  "build:tw3": "cd ./src && yarn install && npm run build"
}

You can name this script whatever you'd like. What it will do is change to the src directory, install the dependencies listed in src/package.json, and then run the build script in src/package.json. You can then run this script in the script that you use to build your production application.

If you're using something like Laravel Mix, then you could have a script like this:

"scripts": {
  "build:tw3": "cd ./src && yarn install && npm run build",
  "production": "npm run build:tw3 && mix --production"
}

The result would be creating a production-optimized stylesheet with the new Tailwind styles. This stylesheet can be included alongside any existing stylesheets, but be sure to pay attention to the order in which the stylesheets are loaded. It's important to load the new stylesheet after the existing stylesheet.

One thing to note, though, is that this will overwrite the original Tailwind version's CSS normalization (Tailwind's Preflight). If you're relying on any of those styles, you may find that they're no longer present in the browser. So you'll have to add them to your tailwind-3.css file.

Adding base files

The next step is to add the base CSS file for the new version. This file is not compiled, so it should be tracked with version control.

In the src directory, add a tailwind-3.css file. You can give this file a different name if you'd like, but be sure to update the watch and build scripts in src/package.json to match the filename that you choose.

In this new file, place the following to add Tailwind's styles:

@tailwind base;
@tailwind components;
@tailwind utilities;

You can add any other styles that utilize classes from Tailwind 3 in this file, such as custom utilities, reusable components, base heading styles, etc. If you have a lot of classes that will go in this file, you can always keep it more organized by importing separate stylesheets.

You also need to include the compiled file (v3.css in the examples we've been using) somewhere in your codebase. As mentioned above, this file should be included after any existing stylesheets.

If you're using Laravel Mix, you'd have something like the following in your webpack.mix.js file:

mix
  // The existing main stylesheet
  .sass('resources/scss/app.scss', 'public/css')
  // The stylesheet for the new Tailwind version
  .css('resources/scss/v3.css', 'public/css')

If you're working in a Nuxt app, then you'd add the file to your nuxt.config.js file, like so:

css: [
  // The existing main stylesheet
  '@/assets/scss/app.scss',
  // The stylesheet for the new Tailwind version
  '@/assets/scss/v3.css',
]

These are just two examples. The location and implementation of loading this stylesheet will vary depending upon the tech stack that your site uses.

Making the updates

Now that everything is added and configured, the upgrade itself can begin.

Running the watcher

First, run your local development environment in one terminal tab. The command for this will depend upon your tech stack, so we won't mention any specific commands here.

In a separate terminal tab, change to the src directory and run the watch command. This will run the Tailwind watcher and recompile your new styles as the tailwind-3.css file is updated. It will also log any errors that exist in that file (or files that are imported into it) in your terminal, so keep an eye on that terminal tab as you make changes.

Note: The watcher will not catch errors for inline classes, so you'll need to double-check those yourself as you update them.

Replacing existing Tailwind classes with new ones

Time to replace existing Tailwind classes with the latest ones. This is where the prefix that you configured before comes in. Swap the existing classes for the corresponding new classes, but include the prefix before every new class. For example:

<!-- Old classes -->
<button class="py-2 px-4 bg-black text-white">
  Sign Up
</button>

<!-- New classes -->
<button class="hc-py-2 hc-px-4 hc-bg-black hc-text-white">
  Sign Up
</button>

Be sure to check for the Tailwind class in the latest version. For example, you may have the pin-t class if you're using Tailwind v0.x. In Tailwind v3, that class is now top-0.

You can also add any new Tailwind classes that didn't exist in older versions and utilize new features. Just be sure to add your prefix before everything that you use (e.g., hc-top-[40px], focus-visible:hc-ring-2, or hc-space-x-4).

You'll also need to add to your src/tailwind.config.js file as you make updates so that the new version is aware of things like custom colors, fonts, etc.

Cleaning up

Once all of the updates have been made, there's some cleanup to do before the upgrade process is truly complete.

Replace the old version of Tailwind

First, the old version of Tailwind needs to be replaced. Upgrade the autoprefixer, postcss, and tailwindcss dependencies by running the following from the root of your codebase:

$ yarn upgrade tailwindcss postcss autoprefixer --latest

Next, delete the tailwind.js (or tailwind.config.js if your site is using v1 or v2) file in the root of your codebase and move src/tailwind.config.js to the root.

If you have an existing postcss.config.js file in the root of your codebase, replace it with src/postcss.config.js. Otherwise, move src/postcss.config.js to the root.

Finally, delete your old stylesheets and move any stylesheets from the src directory into the directory that contained your old stylesheets. You can now change the name of the tailwind-3.css file if you'd like, since it will become the base stylesheet that gets loaded into your site.

Update the area where your stylesheets are loaded (such as the webpack.mix.js or nuxt.config.js file, as shown above) by removing the old stylesheets and pointing to the new path of the tailwind-3.css file.

Note: You no longer need to load the compiled v3.css file, since most frameworks and bundlers will perform CSS optimizations on their own.

Remove the prefix

This step is optional, but if you want to, you can go through your code and remove the prefix. It's no longer necessary now that only one version of Tailwind is being utilized.

If you do remove the prefix, be sure to update your tailwind.config.js file to contain the following:

prefix: ''

Remove files and scripts that are no longer needed

At this point, you can delete the src directory, since it's no longer needed for storing files for a separate Tailwind version.

You can also remove the build:tw3 script in the root package.json file now that a separate script is no longer required for building the new CSS. Be sure to update your root build script to remove any references to the build:tw3 script.

Once the cleanup step is completed, you'll have successfully replaced all of the old Tailwind in your site with the latest version. Piece of cake, right?

Closing thoughts

When we first decided to upgrade one of our sites from Tailwind v0.x to Tailwind v3, we did a lot of research, attempting to determine how it could be done. We expected a somewhat complicated process since we weren't upgrading from one version to the version that directly succeeded it. However, despite quite a few hours of research, we couldn't find anything that detailed how to make such a large jump in versions.

Wanting to avoid the hassle of upgrading from one successive version to the next until we finally got to version 3, we did some tinkering and eventually formulated the process that we've detailed in this post. Since we're sure that we're not the only ones who want to make these upgrades, we wanted to share our process in the hope that it helps anyone else who ends up in the same position.

So, if you find yourself needing to jump from one Tailwind version to a newer one, while skipping the versions in between and being able to make updates in small chunks, we hope that this post helps you. If you have any questions or get stuck at any point as you go through the process, feel free to reach out to us @zaengle.

Want to read more tips and insights on working with a website development team that wants to help your organization grow for good? Sign up for our bimonthly newsletter.

By Rachel Opperman

Engineer

What happens when you cross years of study in biology and medicine with a degree in computer science? You get someone like Rachel.