Upgrading to Tailwind 4 and all that it entails (with a bonus ESLint upgrade!)
From the first Twitter posts with plans for Tailwind 4, it was obvious it would be a more significant upgrade than previous major versions. Its push to simplify the configuration process has greatly reduced the number of packages _tw requires to function, and it also led me to rethink a number of assumptions about the decisions I should be making on behalf of _tw users.
Entering the Tailwind 4 era
I can’t even begin to outline everything that has changed in Tailwind, so I strongly encourage you to read the blog post announcing the release.
If I had to give the briefest of summaries, I would say that it is incredibly fast, easier to configure and modernized in a bunch of important ways. (P3 colors! Container queries! So much else!)
It also encourages its users to go all in with the Tailwind approach, removing previous support for things like Sass-style nesting. (CSS-native nesting is now supported instead by default, with no first-party approach to supporting the old style.)
And this all meant a lot of changes to _tw!
Supporting theme.json
variables in Tailwind 4
Earlier versions of _tw used a custom Tailwind plugin to parse the theme’s theme.json
file and to make available its color palette and content sizing values.
This also required another watch process tracking changes to theme.json
and triggering a Tailwind rebuild when it changed.
This approach both doesn’t work well (or at all) with Tailwind 4 and is made unnecessary by the switch to configuration variables.
You can still set your colors in theme.json
. The change is that now you’ll see them listed as design tokens for Tailwind in ./tailwind/tailwind-theme.css
:
--color-background: var(--wp--preset--color--background);
--color-foreground: var(--wp--preset--color--foreground);
--color-primary: var(--wp--preset--color--primary);
--color-secondary: var(--wp--preset--color--secondary);
--color-tertiary: var(--wp--preset--color--tertiary);
--container-content: var(--wp--style--global--content-size);
--container-wide: var(--wp--style--global--wide-size);
Code language: CSS (css)
This adds the design tokens necessary for classes like bg-background
and text-foreground
, and it assigns them the variables created by WordPress from the values in the theme.json
file.
Removing PostCSS
Tailwind no longer supports running PostCSS via its CLI, and there weren’t enough PostCSS features used by _tw to justify a PostCSS workflow.
(I strongly prefer to use the Tailwind CLI directly, which used to support the --postcss
flag. That flag has been removed.)
Because Tailwind 4 adds functionality equivalent to postcss-import
and renders that plugin unnecessary, and because Tailwind 4 removes tailwindcss/nesting
, the only remaining PostCSS plugin _tw used to use was postcss-import-ext-glob
. This allowed for files in the ./tailwind/custom/components
folder to be automatically imported; this is a nice feature, but in my opinion doesn’t warrant switching from the Tailwind CLI to using PostCSS directly.
Removing Tailwind’s first-party plugins
I used to include all of Tailwind’s first-party plugins for convenience, allowing them to be activated simply by uncommenting them. Only three commented-out first-party plugins remained prior to Tailwind 4’s release. Of those, two are now deprecated:
@tailwindcss/aspect-ratio
@tailwindcss/container-queries
In the case of the former, it supports legacy approaches to aspect ratios in older browsers, but Tailwind 4 itself targets newer browsers than the first to natively support CSS aspect ratios. The latter has been totally subsumed by Tailwind’s core functionality.
That leaves only @tailwindcss/forms
, and I’m not convinced it makes sense to deliver that to all users of _tw by default, so I have removed it.
Tailwind class name ordering in PHP files
Because the first-party Prettier plugin for Tailwind class sorting doesn’t support PHP files in WordPress templates (because Prettier’s PHP plugin doesn’t support inline PHP), _tw used a somewhat complicated ESLint pipeline that would parse the PHP files with @angular-eslint/template-parser
after removing the PHP with eslint-plugin-php-markup
. Then the third-party ESLint plugin for Tailwind—eslint-plugin-tailwindcss
—would handle class sorting.
Unfortunately, that last plugin has not been updated for Tailwind 4, and I didn’t want to hold back Tailwind 4 support in _tw waiting for that update.
In order to support class name ordering in PHP files once again, we’ll either need the above ESLint plugin to be updated for Tailwind 4, or for Prettier’s support for inline PHP to stabilize.
ESLint 9, finally
I have received zero complaints from users about the long wait for ESLint 9 support, but that doesn’t mean I’m not excited to push it to production!
If you haven’t been following along, ESLint 9 deprecated the old approach to configuration via .eslintrc
files, replacing it with flat configuration in an eslint.config.mjs
file.
The upgrade also broke Tailwind class name ordering in PHP files because one of the ESLint plugins _tw relied upon hadn’t been updated to support ESLint 9.
I had already learned the new flat config format and forked the ESLint plugin, rewriting the parts of it we needed to support ESLint 9.
However, with Tailwind 4 eliminating the possibility of Tailwind class name ordering in PHP files for the time being, I was able to focus on supporting ESLint for the basic purpose of linting JavaScript files.
I have also come around to the flat configuration format, and I think it’s an improvement over the old way. Exciting times!
This update is the biggest change to _tw since it launched, so if you have any comments, I’d really love to hear them! I’ve joined Bluesky, and you can find me there as @gregsullivan.com.
I’m excited to start seeing _tw-powered Tailwind 4 sites in the wild, and I look forward to seeing what everyone builds!