This document is only about breaking changes between v1 and v2. For a full list of changes, see the v2.0.0 release.
The tailwind-merge v2 release focuses on making it easier to configure the library for new users.
Over the past one and a half years since the v1 release, the biggest source of issues was the initial configuration of the library. That will unfortunately not change since most of the inconvenience comes from keeping the bundle size to a minimum. However, a few ideas with breaking changes accumulated over time to make the configuration a little bit more straight forward without increasing the bundle size. Those ideas were implemented in this release.
The highlights:
- No changes to
twMerge
and supported Tailwind CSS versions. - You can now override class groups with the object notation in
extendTailwindMerge
. - TypeScript types of configuration object passed to
extendTailwindMerge
are stricter and prevent you from using unknown theme groups and class groups unless specified otherwise. - The library now exports a bundle using modern JS syntax to further reduce its size. There is also an additional bundle with ES5-only syntax for compatibility with older browsers.
If you have feedback of any kind regarding this release, feel free to open an issue or discussion. I'm always happy to hear from you!
By exports:
- All
extendTailwindMerge
validators
createTailwindMerge
fromTheme
mergeConfigs
join
The build of tailwind-merge was updated to output more modern JS syntax. This removes a lot of code to substitute modern JS features like object spread or arrow functions, but also means that the new bundle doesn't work in older browsers anymore in case you don't transpile your dependencies.
The exact browserslist query used to define how tailwind-merge gets transpiled from now on is
> 0.5%, last 2 versions, Firefox ESR, not dead, maintained node versions
which corresponds to the browserslist query defaults
with the addition of maintained node versions
.
In case you need to support older browsers and don't transpile your dependencies, you can use the new tailwind-merge/es5
bundle which includes all of the tailwind-merge exports and is transpiled to ES5 syntax using the browserslist query supports es5
.
Replace all imports of tailwind-merge
with tailwind-merge/es5
.
- import { … } from 'tailwind-merge'
+ import { … } from 'tailwind-merge/es5'
Check whether the browserslist query > 0.5%, last 2 versions, Firefox ESR, not dead, maintained node versions
includes all browsers you need to support. If not, replace all imports of tailwind-merge
with tailwind-merge/es5
as shown in the minimal upgrade above.
Related: #289
The module
field was removed from the package.json
file of tailwind-merge.
This field was briefly introduced by bundlers to import the ES module version of a library. But it was ultimately succeeded by the exports
field which became a standard in Node.js and is also used by tailwind-merge. All major bundlers support the exports
field, so removing the module
field should hopefully not affect anyone. However, if you encounter any problems with this change, please open an issue.
If you can import tailwind-merge and your module resolution system doesn't complain, there is nothing you need to do. If you encounter issues, make sure that you resolve the tailwind-merge library via the exports
or main
field of its package.json
file.
Related: #294
In the object passed to extendTailwindMerge
it is now possible to not just extend groups, but also to override them. To distinguish between extending and overriding, there are two new top level keys in the object: extend
and override
. Both of these objects can define a theme
, classGroups
, conflictingClassGroups
and conflictingClassGroupModifiers
object whose groups will either override or extend the corresponding groups in the default config.
You might remember the extend
key in your tailwind.config.js
, now it works similarly in tailwind-merge as well. However, you need to use the override
object instead of defining groups at the top level to override. This was done to make overriding more explicit and to prevent accidentally overriding groups when migrating to tailwind-merge v2.
To get the same behavior as before, move theme
, classGroups
, conflictingClassGroups
and conflictingClassGroupModifiers
into an extend
object.
import { extendTailwindMerge } from 'tailwind-merge'
const twMerge = extendTailwindMerge({
cacheSize: 1000,
- theme: { … },
- classGroups: { … },
- conflictingClassGroups: { … },
- conflictingClassGroupModifiers: { … },
+ extend: {
+ theme: { … },
+ classGroups: { … },
+ conflictingClassGroups: { … },
+ conflictingClassGroupModifiers: { … },
+ }
})
If you override groups in your tailwind.config.js
, you can now do the same in tailwind-merge. Move the groups you want to override into the override
object and the rest into the extend
object.
import { extendTailwindMerge } from 'tailwind-merge'
const twMerge = extendTailwindMerge({
- classGroups: {
- // Here we actually meant to override this group
- shadow: [{ shadow: ['none', '100', '200', '300'] }],
- // Here we really meant to extend this group
- animate: ['animate-shimmer'],
- },
+ override: {
+ classGroups: {
+ // Now we can override this group
+ shadow: [{ shadow: ['none', '100', '200', '300'] }],
+ },
+ },
+ extend: {
+ classGroups: {
+ // And we can still extend this group
+ animate: ['animate-shimmer'],
+ },
+ },
})
Related: #279
One of the more annoying things about configuring tailwind-merge is to find the right IDs of groups to edit. Some users also fall into the trap that not all theme groups from Tailwind CSS are supported in tailwind-merge.
To add some guidance to the process, all the group objects passed to extendTailwindMerge
don't accept any string keys in TypeScript anymore but only a set of specific keys which are defined in the default config.
import { extendTailwindMerge } from 'tailwind-merge'
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
// ↓ No problem
shadow: [{ shadow: ['100', '200', '300'] }],
// ↓ Error: Object literal may only specify known properties, and 'animation' does not
// exist in type 'Partial<Record<DefaultClassGroupIds, ClassGroup<DefaultThemeGroupIds>>>'.
animation: ['animate-shimmer'],
},
},
})
This means you can't accidentally extend or override a group with a typo in the ID anymore without noticing and you get code completion when you start typing the ID of a group in your editor.
But this also means that you can't add any new groups to the configuration object anymore. To allow custom class group IDs, you can use generic arguments to extendTailwindMerge
to pass additional allowed IDs for class groups and theme groups separately.
import { extendTailwindMerge } from 'tailwind-merge'
const twMerge = extendTailwindMerge<
// ↓ Add additional class group IDs as the first generic argument
'class-a' | 'class-b',
// ↓ Optionally, you can add additional theme group IDs as the second generic argument
'theme-c' | 'theme-d'
>({
extend: {
theme: {
// ↓ No problem since we defined 'theme-c' as allowed theme group ID
'theme-c': […],
// ↓ No problem since we defined 'theme-d' as allowed theme group ID
'theme-d': […],
},
classGroups: {
// ↓ No problem since it's part of the default allowed class group IDs
shadow: […],
// ↓ No problem since we defined 'class-a' as allowed class group ID
'class-a': […],
// ↓ No problem since we defined 'class-b' as allowed class group ID
'class-b': […],
},
},
})
To get the the same behavior as before, allow any group IDs by passing string
as generic arguments to extendTailwindMerge
.
import { extendTailwindMerge } from 'tailwind-merge'
- const twMerge = extendTailwindMerge({
+ const twMerge = extendTailwindMerge<string, string>({
extend: {
theme: { … },
classGroups: { … },
},
})
If you aren't using any custom groups, there is nothing you need to do to upgrade.
If you use any custom class group or theme group IDs in your tailwind-merge config, pass them as generic arguments to extendTailwindMerge
.
import { extendTailwindMerge } from 'tailwind-merge'
+ type AdditionalClassGroupIds = 'class-a' | 'class-b'
+ type AdditionalThemeGroupIds = 'theme-c' | 'theme-d'
- const twMerge = extendTailwindMerge({
+ const twMerge = extendTailwindMerge<AdditionalClassGroupIds, AdditionalThemeGroupIds>({
// your config
})
You can also pass never
to any argument if you don't use any custom groups of that type.
If you still see TypeScript errors on object keys of class groups or theme groups, check in the default config whether the group ID is defined there. It might be that you configured tailwind-merge incorrectly.
Related: #292
To be consistent with other validators and to make validators more composable, each validator only checks for class parts with either arbitrary values or non-arbitrary values. The isLength
validator previously checked for both which made it inconsistent with the rest. Now, isLength
does not check for arbitrary lengths anymore.
Compose the isLength
and isArbitraryLength
validators to get the same behavior as before. You can use one of several options:
import { validators } from 'tailwind-merge'
+ function isLengthOrArbitraryLength(value: string) {
+ return validators.isLength(value) || validators.isArbitraryLength(value)
+ }
- validators.isLength
+ isLengthOrArbitraryLength
import { validators } from 'tailwind-merge'
const twMerge = createTailwindMerge(() => ({
cacheSize: 500,
separator: ':',
theme: {},
classGroups: {
- mySpecialGroup: [{ special: [validators.isLength] }],
+ mySpecialGroup: [{ special: [validators.isLength, validators.isArbitraryLength] }],
},
conflictingClassGroups: {},
conflictingClassGroupModifiers: {},
}))
Related: #292
Same as the isLength
validator: To be consistent with other validators and to make validators more composable, each validator only checks for class parts with either arbitrary values or non-arbitrary values. The isInteger
validator previously checked for both which made it inconsistent with the rest. Now, isInteger
does not check for arbitrary integers anymore.
tailwind-merge doesn't export an isArbitraryInteger
validator because it doesn't use one in the default config (it uses isArbitraryValue
instead because there are no ambiguous arbitrary values in the default config). You'll need to rebuild the previous functionality.
import { validators } from 'tailwind-merge'
+ function isIntegerOrArbitraryInteger(value: string) {
+ return validators.isInteger(value) || /^\[(number:.+|-?\d+)\]$/.test(value)
+ }
- validators.isInteger
+ isIntegerOrArbitraryInteger
If the classes where you previously used validators.isInteger
don't use arbitrary values, you can continue to use it without any changes.
If those classes use arbitrary values but there is only a single class group that could use the arbitrary value, you don't need to check that the arbitrary value is an integer, only that it is any arbitrary value. E.g. for the class px-[<any-value-in-here>]
we don't need to know what is between the brackets because only the px
class group uses the px-
prefix. In that case you can compose isInteger
and isArbitraryValue
as shown in the upgrade section for validators.isLength
.
Otherwise, proceed as shown in the minimal upgrade.
Related: #300
isArbitraryUrl
was used to detect arbitrary background-image
values. However, the background-image
CSS property supports more than just URLs, so the functionality of the validator was expanded to also detect values like image:var(--maybe-an-image-at-runtime)]
or linear-gradient(rgba(0,0,255,0.5),rgba(255,255,0,0.5))
and therefore renamed as well.
Replace all uses of validators.isArbitraryUrl
with validators.isArbitraryImage
.
import { validators } from 'tailwind-merge'
- validators.isArbitraryUrl
+ validators.isArbitraryImage
Related: #288
The validator was renamed to validators.isArbitraryNumber
in v1.6.2
to better reflect what it's doing. validators.isArbitraryWeight
has been deprecated since then and was removed in this release.
Replace all uses of validators.isArbitraryWeight
with validators.isArbitraryNumber
.
import { validators } from 'tailwind-merge'
- validators.isArbitraryWeight
+ validators.isArbitraryNumber
The separator
and conflictingClassGroupModifiers
keys became mandatory in the configuration object returned in the createTailwindMerge
callback since tailwind-merge defines those defaults only in its default config. This change makes processing for plugins easier.
If you don't have those keys defined in the configuration object, add them with their default values.
import { createTailwindMerge } from 'tailwind-merge'
const twMerge = createTailwindMerge(() => {
return {
cacheSize: 500,
+ separator: ':',
theme: {},
classGroups: {},
conflictingClassGroups: {},
+ conflictingClassGroupModifiers: {},
}
})
Related: #279
Matching the stricter types in extendTailwindMerge
, the fromTheme
function now only accepts theme group IDs that are defined in the default config.
You can define additional allowed theme group IDs by passing them as generic arguments to fromTheme
. If you don't use the default config, you can also redefine the default theme group IDs by passing them as a second generic arguments to fromTheme
.
To get the the same behavior as before, allow any theme group IDs by passing string
as generic arguments to fromTheme
.
import { fromTheme } from 'tailwind-merge'
- const myThemeGroup = fromTheme('my-theme-group')
+ const myThemeGroup = fromTheme<string>('my-theme-group')
If you don't use any custom theme groups, there is nothing you need to do to upgrade.
If you use any custom theme groups in your tailwind-merge config, pass them as a generic argument to fromTheme
.
import { fromTheme } from 'tailwind-merge'
+ type AdditionalThemeGroupIds = 'my-theme-group' | 'my-other-theme-group'
- const customThemeGroup = fromTheme('my-theme-group')
+ const customThemeGroup = fromTheme<AdditionalThemeGroupIds>('my-theme-group')
In case you're not extending the default config but defining a config from scratch with createTailwindMerge
, you can also redefine the default theme group IDs by passing them as the second generic arguments to fromTheme
.
import { fromTheme } from 'tailwind-merge'
+ type ThemeGroupIds = 'my-theme-group' | 'my-other-theme-group'
- const customThemeGroup = fromTheme('my-theme-group')
+ const customThemeGroup = fromTheme<never, ThemeGroupIds>('my-theme-group')
Related: #279
mergeConfigs
is used internally within extendTailwindMerge
, therefore the object shape change in extendTailwindMerge
applies to mergeConfigs
as well. Please read extendTailwindMerge
: Object shape changed for the details and upgrade instructions.
Related: #279
The reason and effect of the stricter TypeScript types is the same as in extendTailwindMerge
: Stricter TypeScript types. The main difference in mergeConfigs
is that the generic arguments don't accept additional group IDs but rather all allowed group IDs since mergeConfigs
is independent of the default config.
To get the the same behavior as before, allow any group IDs by passing string
as generic arguments to mergeConfigs
.
import { mergeConfigs } from 'tailwind-merge'
- mergeConfigs(baseConfig, {
+ mergeConfigs<string, string>(baseConfig, {
extend: {
theme: { … },
classGroups: { … },
}
})
You need to define all the class group IDs and theme group IDs you use in two string literal union types and pass them as generic arguments to mergeConfigs
unless TypeScript inference can infer the types.
import { mergeConfigs } from 'tailwind-merge'
+ type ClassGroupIds = 'class-a' | 'class-b'
+ type ThemeGroupIds = 'theme-c' | 'theme-d'
- mergeConfigs(baseConfig, {
+ mergeConfigs<ClassGroupIds, ThemeGroupIds>(baseConfig, {
extend: {
theme: { … },
classGroups: { … },
}
})
Related: #288
The join
function was renamed to twJoin
in v1.8.0
to distinguish it better from other functions with the same name in your codebase. join
has been deprecated since then and was removed in this release.
Replace all uses of join
with twJoin
.
- import { join } from 'tailwind-merge'
+ import { twJoin } from 'tailwind-merge'
- join
+ twJoin