Jeremy Smith on February 09, 2025

Upgrading a Rails App to Tailwind v4

Last week, I upgraded a client’s Rails app to Tailwind v4. I rarely delay upgrading dependencies, but after briefly looking through the release notes on Tailwind v4 when it first came out, I decided I would hold off for a bit on most of my projects. (I also tend to wait a bit on upgrading to a major release to until a few patch releases have come out.) For this modest-sized Rails app, the upgrade took me somewhere between 4-6 hours, mainly because I was struggling to figure out everything that changed, and how best to use the upgrade tool. If your setup is similar to ours, hopefully this will save you some time.

This post is going to be most relevant for Rails apps using Tailwind with PostCSS. If you’re using the tailwindcss-rails gem, you should read Rob Zolkos’ post and follow the upgrade section of the gem’s README.

But, no matter what your setup, you are going to need to read through the Tailwind Upgrade guide. Maybe even a couple times.

Do You Still Need PostCSS?

If you’ve previously chosen PostCSS for your app for legacy reasons (e.g. supporting SCSS), or for something like minification through another library (e.g. cssnano), it may be that you can switch to using Tailwind CLI instead. That’s what I was able to do, and dropped several dependencies, which was quite satisfying. In particular: autoprefixer, cssnano, postcss, postcss-cli, postcss-flexbugs-fixes, and postcss-import.

Here’s why:

In v3, the tailwindcss package was a PostCSS plugin, but in v4 the PostCSS plugin lives in a dedicated @tailwindcss/postcss package.

Additionally, in v4 imports and vendor prefixing is now handled for you automatically, so you can remove postcss-import and autoprefixer if they are in your project:

Using PostCSS

Major Considerations

At a high level, here are the major things to consider regarding this upgrade:

  1. There are quite a few class removals and renames in this major release. You want to use the upgrade tool and not make the changes by hand. (At first, I had trouble getting the upgrade tool to work, so I started a new branch to make the class changes manually. After 20 minutes or so, I realized it was going to take an unreasonably long time and there was a high likelihood I would make mistakes and possibly go blind from all the tedious line changes.)

  2. If you’ve been using a Javascript config file (i.e. tailwind.config.js), it might be time to stop. This is no longer the recommended way of configuring Tailwind. You can probably do what you need with directives or with newer element modifiers.

  3. There are some new defaults that you may have to make manual changes for, or accept some visual differences. In particular: default border color, focus ring width/color, placeholder color, and button cursor.

  4. This major release is probably not going to be diffable. Often, when I’m concerned about a CSS dependency change, I’ll precompile the CSS locally on the main branch and the new upgrade branch (without minification), then diff the two files and spot-check to ensure the actual changes are consistent with release notes. This major version change is too significant, so you probably need to rely on visual QA instead.

Now for some notes that were more particular to my upgrade…

Typography Configuration

I realized well into the process that JS config is now out of style.

JavaScript config files are still supported for backward compatibility, but they are no longer detected automatically in v4.

Using a JavaScript config file

I checked my own config and found that it was all customizations for @tailwindcss/typography. So I gutted the config, switched everything to element modifiers, and deployed that change ahead of the upgrade.

Running the Upgrade Tool

I had a lot of trouble getting the upgrade tool to run properly. Eventually I found I needed to do the following:

  • Have Tailwind v4 installed (to run the upgrade tool), but be on a branch referencing v3
  • Rename some stylesheet file extensions that were still .scss to .css (whoops!)
  • Add a leading . to all content paths in config (hat tip, tailwindcss-rails gem)

The upgrade tool might be a little overzealous. For example, I found it tried to change a Stimulus action from blur->autosuggest#blur to blur->autosuggest#blur-sm because the blur class has changed to blur-sm. Otherwise, it was great! It made changes to hundreds of files and I only had to manually review the changes.

In the end, my package.json had the following changes:

  • @tailwindcss/cli under dependencies
  • tailwindcss, @tailwindcss/forms, and @tailwindcss/typography under devDependencies
  • the build script switched from using postcss to @tailwindcss/cli

Here’s an abridged version:

{
  "dependencies": {
    "@tailwindcss/cli": "^4.0.4",
  },
  "devDependencies": {
    "@tailwindcss/forms": "^0.5.10",
    "@tailwindcss/typography": "^0.5.16",
    "tailwindcss": "^4.0.4"
  },
  "scripts": {
    "build:css": "npx @tailwindcss/cli -i ./app/assets/stylesheets/application.css -o ./app/assets/builds/application.css --minify"
  }
}

I use foreman to manage local processes, and thankfully my Procfile.dev didn’t need to change, as the --watch flag is passed through just fine.

web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
js: yarn build --watch
css: yarn build:css --watch

Dealing with New Defaults

To retain previous defaults on border color and button cursor, I added these overrides to my application.css. (Actually, the upgrade tool inserted the border color style, and I copied in the button cursor style from the guide.)

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }

  button:not(:disabled),
  [role="button"]:not(:disabled) {
    cursor: pointer;
  }
}

The thing that bugged me the most was the change to focus ring. In the end, to get close to what I had before, I added the following to form input classes. It’s not quite the same, as I believe the focus ring in v3 would cover the border, but in v4 is outside the border. But it was acceptable.

Before:

"w-full border-gray-300 shadow-inner disabled:bg-gray-100"

After:

"w-full border-gray-300 shadow-inner disabled:bg-gray-100 focus:ring-2 focus:ring-blue-600"

Good luck with your own upgrade to Tailwind v4! While it’s a bit of a pain, I don’t think it’s nearly as bad as upgrading from Bootstrap v3 to v4. Remember that one? I’d love to know what percentage of projects got stuck on v3 and never upgraded after that.

Postscript

This post started out as a Twitter thread. If you have questions or suggestions, feel free to reply there and I’ll try to respond!


Need help building or maintaining a Rails app?

Book a one-hour project inquiry call or reach out via email.