On 22 January, 3 days ago at the time of writing, Tailwind v4.0 was released with some major changes. I decided to upgrade today, and the upgrade path was mixed to be honest!
Tailwind provides an automated upgrade tool, which worked okay-ish. One of the major changes is the new CSS-first configuration instead of the old JavaScript-based configuration. Unfortunately, the upgrade tool didn’t migrate the configuration, saying it couldn’t. Maybe because of the typography plugin, maybe because of something else, I don’t know, it didn’t tell me.
But the new CSS-based config is a big part of Tailwind 4, and I wanted to use it. The upgrade tool simply loaded the old config in compatibility mode.
So I had to migrate it manually, but the upgrade guide doesn’t really explain how to migrate the old JavaScript-based configuration. So I had to figure it out for myself, and while some of it was obvious and easy to find, other things weren’t so easy.
I use multiple dark mode variants. This is what it looked like in the old configuration:
The variant can now be configured with the directive. The Dark mode docs have an example of how to override it with a single rule, but how do I use it with multiple rules like in the old configuration? It took me a while to figure out that the docs use a shorthand syntax, and you can use multiple rules with the long syntax:
Tailwind’s color palette has 5 different gray temperatures, ranging from a cool blueish gray called “Slate” to a warm brownish gray called “Stone”. You usually use only one of these grays. I like to use “Stone” and have aliased it to .
How does this aliasing work in the new CSS-based configuration? Well, you have to override all gray shades.
To create a utility class with a custom background image (for an image file, not a gradient), you could extend the theme like any other utility:
The new docs to customize only mention gradients, but not images. So I guessed how it should work based on how it used to work, but I guessed wrong. I also couldn’t figure out if there’s still a way to extend it within the theme, so I’m now simply using a utility.
I have some places where I only want to transition the and properties, and I simply used to apply these transitions. But after upgrading to v4, these transitions were broken! I couldn’t find anything in the upgrade guide, and the transition-property docs didn’t look any different at first glance.
Until I saw that now uses 4 properties: the usual , but 3 new custom properties , and . This is not mentioned as a breaking change, and the upgrade tool didn’t catch it either.
You now have to transition those properties as well:
Of course, you don’t need to specify all of these properties if you only need to transition some of them.
Tailwind’s typography plugin is great, and I think you should always use it (or any alternative for good standard text rendering). When I was manually migrating the configuration, I finally got to the typography configuration and was wondering how to migrate it to the new CSS-based configuration. The documentation of the plugin didn’t mention the new config, and the upgrade guide didn’t mention anything either. Until I found a note in the functions and directives docs:
@plugin
Use the directive to load a legacy JavaScript-based plugin:
Legacy? Oh no. Let’s hope we get non-legacy plugins in the future.
Anyway, to use the typography plugin, you can use the old configuration and import it into the new configuration.
Although the upgrade path has been a bit of a bumpy ride, I like the overall change in direction that Tailwind has taken. It now not only supports the new features of the platform, it embraces them and shows them to you. It’s much more of an extension of the platform, rather than just a collection of utility classes.
On 22 January, 3 days ago at the time of writing, Tailwind v4.0 was released with some major changes. I decided to upgrade today, and the upgrade path was mixed to be honest!
Tailwind provides an automated upgrade tool, which worked okay-ish. One of the major changes is the new CSS-first configuration instead of the old JavaScript-based configuration. Unfortunately, the upgrade tool didn’t migrate the configuration, saying it couldn’t. Maybe because of the typography plugin, maybe because of something else, I don’t know, it didn’t tell me.
But the new CSS-based config is a big part of Tailwind 4, and I wanted to use it. The upgrade tool simply loaded the old config in compatibility mode.
So I had to migrate it manually, but the upgrade guide doesn’t really explain how to migrate the old JavaScript-based configuration. So I had to figure it out for myself, and while some of it was obvious and easy to find, other things weren’t so easy.
I use multiple dark mode variants. This is what it looked like in the old configuration:
The variant can now be configured with the directive. The Dark mode docs have an example of how to override it with a single rule, but how do I use it with multiple rules like in the old configuration? It took me a while to figure out that the docs use a shorthand syntax, and you can use multiple rules with the long syntax:
Tailwind’s color palette has 5 different gray temperatures, ranging from a cool blueish gray called “Slate” to a warm brownish gray called “Stone”. You usually use only one of these grays. I like to use “Stone” and have aliased it to .
How does this aliasing work in the new CSS-based configuration? Well, you have to override all gray shades.
To create a utility class with a custom background image (for an image file, not a gradient), you could extend the theme like any other utility:
The new docs to customize only mention gradients, but not images. So I guessed how it should work based on how it used to work, but I guessed wrong. I also couldn’t figure out if there’s still a way to extend it within the theme, so I’m now simply using a utility.
I have some places where I only want to transition the and properties, and I simply used to apply these transitions. But after upgrading to v4, these transitions were broken! I couldn’t find anything in the upgrade guide, and the transition-property docs didn’t look any different at first glance.
Until I saw that now uses 4 properties: the usual , but 3 new custom properties , and . This is not mentioned as a breaking change, and the upgrade tool didn’t catch it either.
You now have to transition those properties as well:
Of course, you don’t need to specify all of these properties if you only need to transition some of them.
Tailwind’s typography plugin is great, and I think you should always use it (or any alternative for good standard text rendering). When I was manually migrating the configuration, I finally got to the typography configuration and was wondering how to migrate it to the new CSS-based configuration. The documentation of the plugin didn’t mention the new config, and the upgrade guide didn’t mention anything either. Until I found a note in the functions and directives docs:
@plugin
Use the directive to load a legacy JavaScript-based plugin:
Legacy? Oh no. Let’s hope we get non-legacy plugins in the future.
Anyway, to use the typography plugin, you can use the old configuration and import it into the new configuration.
Although the upgrade path has been a bit of a bumpy ride, I like the overall change in direction that Tailwind has taken. It now not only supports the new features of the platform, it embraces them and shows them to you. It’s much more of an extension of the platform, rather than just a collection of utility classes.
dark:@custom-variantgraybackground-imagetransition-propertytransformopacitytransition-[transform,opacity]transition-transformtransformtranslatescalerotate@plugin/* This is how the new configuration file should look like */
@import "tailwindcss";
@theme {
--font-display: "Satoshi", "sans-serif";
--breakpoint-3xl: 1920px;
--color-avocado-100: oklch(0.99 0 0);
--color-avocado-200: oklch(0.98 0.04 113.22);
--color-avocado-300: oklch(0.94 0.11 115.03);
--color-avocado-400: oklch(0.92 0.19 114.08);
--color-avocado-500: oklch(0.84 0.18 117.33);
--color-avocado-600: oklch(0.53 0.12 118.34);
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
--ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}
/* This is what the upgrade tool did */
@config "../../tailwind.config.js";
@plugin "@tailwindcss/typography";
dark:@custom-variantgraybackground-imagetransition-propertytransformopacitytransition-[transform,opacity]transition-transformtransformtranslatescalerotate@plugin/* This is how the new configuration file should look like */
@import "tailwindcss";
@theme {
--font-display: "Satoshi", "sans-serif";
--breakpoint-3xl: 1920px;
--color-avocado-100: oklch(0.99 0 0);
--color-avocado-200: oklch(0.98 0.04 113.22);
--color-avocado-300: oklch(0.94 0.11 115.03);
--color-avocado-400: oklch(0.92 0.19 114.08);
--color-avocado-500: oklch(0.84 0.18 117.33);
--color-avocado-600: oklch(0.53 0.12 118.34);
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
--ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}
/* This is what the upgrade tool did */
@config "../../tailwind.config.js";
@plugin "@tailwindcss/typography";
@config './typography.config.js'
@theme {
/* ... */
}
@config './typography.config.js'
@theme {
/* ... */
}
// typography.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
plugins: [require('@tailwindcss/typography')],
theme: {
extend: {
typography: () => ({
DEFAULT: {
css: {
color: '#333',
a: {
color: '#3182ce',
'&:hover': {
color: '#2c5282',
}
}
// etc
// typography.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
plugins: [require('@tailwindcss/typography')],
theme: {
extend: {
typography: () => ({
DEFAULT: {
css: {
color: '#333',
a: {
color: '#3182ce',
'&:hover': {
color: '#2c5282',
}
}
// etc


@theme {
/* This does not work! */
--bg-dark-grain: url("dark-grain-pattern.svg");
--bg-light-grain: url("light-grain-pattern.svg");
}
/* This works */
@utility bg-dark-grain {
background-image: url("dark-grain-pattern.svg");
}
@utility bg-light-grain {
background-image: url("light-grain-pattern.svg");
}
module.exports = {
darkMode: [
'variant',
[
'@media (prefers-color-scheme: dark) { &:not(html[data-theme=light] *, [data-theme=light]) }',
'&:is([data-theme=dark] *, html[data-theme=dark])',
],
]
}
/* Example in the docs, uses the shorthand syntax */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
/* This is how it works with multiple selectors */
@custom-variant dark {
&:where([data-theme='dark'] *, [data-theme='dark']) {
@slot;
}
@media (prefers-color-scheme: dark) {
&:not(html[data-theme='light'] *, [data-theme='light']) {
@slot;
}
}
}
@theme {
/* This does not work! */
--bg-dark-grain: url("dark-grain-pattern.svg");
--bg-light-grain: url("light-grain-pattern.svg");
}
/* This works */
@utility bg-dark-grain {
background-image: url("dark-grain-pattern.svg");
}
@utility bg-light-grain {
background-image: url("light-grain-pattern.svg");
}
module.exports = {
darkMode: [
'variant',
[
'@media (prefers-color-scheme: dark) { &:not(html[data-theme=light] *, [data-theme=light]) }',
'&:is([data-theme=dark] *, html[data-theme=dark])',
],
]
}
/* Example in the docs, uses the shorthand syntax */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
/* This is how it works with multiple selectors */
@custom-variant dark {
&:where([data-theme='dark'] *, [data-theme='dark']) {
@slot;
}
@media (prefers-color-scheme: dark) {
&:not(html[data-theme='light'] *, [data-theme='light']) {
@slot;
}
}
}
const colors = require('tailwindcss/colors')
module.exports = {
theme: {
extends: {
colors: {
gray: colors.stone,
}
}
}
}
<!-- v3 -->
<div class="transition-[transform,opacity]" />
<!-- v4 -->
<div class="transition-[transform,translate,scale,rotate,opacity]" />
module.exports = {
theme: {
extends: {
backgroundImage: {
'dark-grain': "url(dark-grain-pattern.svg)",
'light-grain': "url(light-grain-pattern.svg)"
}
}
}
}
const colors = require('tailwindcss/colors')
module.exports = {
theme: {
extends: {
colors: {
gray: colors.stone,
}
}
}
}
<!-- v3 -->
<div class="transition-[transform,opacity]" />
<!-- v4 -->
<div class="transition-[transform,translate,scale,rotate,opacity]" />
module.exports = {
theme: {
extends: {
backgroundImage: {
'dark-grain': "url(dark-grain-pattern.svg)",
'light-grain': "url(light-grain-pattern.svg)"
}
}
}
}
@theme {
--color-gray-50: var(--color-stone-50);
--color-gray-100: var(--color-stone-100);
--color-gray-200: var(--color-stone-200);
--color-gray-300: var(--color-stone-300);
--color-gray-400: var(--color-stone-400);
--color-gray-500: var(--color-stone-500);
--color-gray-600: var(--color-stone-600);
--color-gray-700: var(--color-stone-700);
--color-gray-800: var(--color-stone-800);
--color-gray-900: var(--color-stone-900);
--color-gray-950: var(--color-stone-950);
}
@theme {
--color-gray-50: var(--color-stone-50);
--color-gray-100: var(--color-stone-100);
--color-gray-200: var(--color-stone-200);
--color-gray-300: var(--color-stone-300);
--color-gray-400: var(--color-stone-400);
--color-gray-500: var(--color-stone-500);
--color-gray-600: var(--color-stone-600);
--color-gray-700: var(--color-stone-700);
--color-gray-800: var(--color-stone-800);
--color-gray-900: var(--color-stone-900);
--color-gray-950: var(--color-stone-950);
}