Repository: dcastil/tailwind-merge (ts)

Issue:
Add support for Tailwind CSS v4.1
Announced in Tweet https://x.com/adamwathan/status/1905670109441446391

> Tailwind CSS v4.1 coming next week 🤙🏻
>
> ![Image](https://github.com/user-attachments/assets/c75c9702-4812-4ea2-bfee-1f533ef2f528)

# Changes to check

Everything in https://github.com/tailwindlabs/tailwindcss/releases/tag/v4.1.0.

### Added

- [x] Add `details-content` variant ([#15319](https://github.com/tailwindlabs/tailwindcss/pull/15319))
- [x] Add `inverted-colors` variant ([#11693](https://github.com/tailwindlabs/tailwindcss/pull/11693))
- [x] Add `noscript` variant ([#11929](https://github.com/tailwindlabs/tailwindcss/pull/11929), [#17431](https://github.com/tailwindlabs/tailwindcss/pull/17431))
- [x] Add `items-baseline-last` and `self-baseline-last` utilities ([#13888](https://github.com/tailwindlabs/tailwindcss/pull/13888), [#17476](https://github.com/tailwindlabs/tailwindcss/pull/17476))
- [x] Add `pointer-none`, `pointer-coarse`, and `pointer-fine` variants ([#16946](https://github.com/tailwindlabs/tailwindcss/pull/16946))
- [x] Add `any-pointer-none`, `any-pointer-coarse`, and `any-pointer-fine` variants ([#16946](https://github.com/tailwindlabs/tailwindcss/pull/16946))
- [x] Add safe alignment utilities ([#14607](https://github.com/tailwindlabs/tailwindcss/pull/14607))
- [x] Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
- [x] Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
- [x] Add `@source inline(…)` and `@source not inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
- [x] Add `@source not "…"` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
- [x] Add `text-shadow-*` utilities ([#17389](https://github.com/tailwindlabs/tailwindcss/pull/17389))
- [x] Add `mask-*` utilities ([#17134](https://github.com/tailwindlabs/tailwindcss/pull/17134))
- [x] Add `bg-{position,size}-*` utilities for arbitrary values ([#17432](https://github.com/tailwindlabs/tailwindcss/pull/17432))
- [x] Add `shadow-*/<alpha>`, `inset-shadow-*/<alpha>`, `drop-shadow-*/<alpha>`, and `text-shadow-*/<alpha>` utilities to control shadow opacity ([#17398](https://github.com/tailwindlabs/tailwindcss/pull/17398), [#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434))
- [x] Add `drop-shadow-<color>` utilities ([#17434](https://github.com/tailwindlabs/tailwindcss/pull/17434))
- [x] Improve compatibility with older versions of Safari and Firefox ([#17435](https://github.com/tailwindlabs/tailwindcss/pull/17435))

### Fixed

- [x] Follow symlinks when resolving `@source` directives ([#17391](https://github.com/tailwindlabs/tailwindcss/pull/17391))
- [x] Don't scan ignored files for classes when changing an ignored file triggers a rebuild using `@tailwindcss/cli` ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
- [x] Support negated `content` rules in legacy JavaScript configuration ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
- [x] Interpret syntax like `@("@")md:…` as `@md:…` in Razor files ([#17427](https://github.com/tailwindlabs/tailwindcss/pull/17427))
- [x] Disallow top-level braces, top-level semicolons, and unbalanced parentheses and brackets in arbitrary values ([#17361](https://github.com/tailwindlabs/tailwindcss/pull/17361))
- [x] Ensure the `--theme(…)` function still resolves to the CSS variables when using legacy JS plugins ([#17458](https://github.com/tailwindlabs/tailwindcss/pull/17458))
- [x] Detect used theme variables in CSS module files ([#17433](https://github.com/tailwindlabs/tailwindcss/pull/17433), [#17467](https://github.com/tailwindlabs/tailwindcss/pull/17467))

### Changed

- [x] Ignore `node_modules` by default (can be overridden by `@source …` rules) ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
- [x] `@source` rules that include file extensions or point inside `node_modules/` folders no longer consider your `.gitignore` rules ([#17255](https://github.com/tailwindlabs/tailwindcss/pull/17255))
- [x] Deprecate `bg-{left,right}-{top,bottom}` in favor of `bg-{top,bottom}-{left,right}` utilities ([#17378](https://github.com/tailwindlabs/tailwindcss/pull/17378))
- [x] Deprecate `object-{left,right}-{top,bottom}` in favor of `object-{top,bottom}-{left,right}` utilities ([#17437](https://github.com/tailwindlabs/tailwindcss/pull/17437))

Pull request:
Add support for Tailwind CSS v4.1
Closes #556

Patch:
diff --git a/docs/README.md b/docs/README.md
index 75bafea..2c0a284 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -16,7 +16,7 @@ twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
 // → 'hover:bg-dark-red p-3 bg-[#B91C1C]'
 ```
 
-- Supports Tailwind v4.0 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0))
+- Supports Tailwind v4.0 up to v4.1 (if you use Tailwind v3, use [tailwind-merge v2.6.0](https://github.com/dcastil/tailwind-merge/tree/v2.6.0))
 - Works in all modern browsers and maintained Node versions
 - Fully typed
 - [Check bundle size on Bundlephobia](https://bundlephobia.com/package/tailwind-merge)
diff --git a/docs/features.md b/docs/features.md
index 41be44b..ba0329f 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -33,7 +33,7 @@ twMerge('hover:p-2 hover:p-4') // → 'hover:p-4'
 twMerge('hover:focus:p-2 focus:hover:p-4') // → 'focus:hover:p-4'
 ```
 
-The order of standard modifiers does not matter for tailwind-merge.
+tailwind-merge knows when the order of standard modifiers matters and when not and resolves conflicts accordingly.
 
 ### Supports arbitrary values
 
diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts
index b332426..fbafd88 100644
--- a/src/lib/default-config.ts
+++ b/src/lib/default-config.ts
@@ -43,6 +43,7 @@ export const getDefaultConfig = () => {
     const themeRadius = fromTheme('radius')
     const themeShadow = fromTheme('shadow')
     const themeInsetShadow = fromTheme('inset-shadow')
+    const themeTextShadow = fromTheme('text-shadow')
     const themeDropShadow = fromTheme('drop-shadow')
     const themeBlur = fromTheme('blur')
     const themePerspective = fromTheme('perspective')
@@ -62,16 +63,26 @@ export const getDefaultConfig = () => {
         ['auto', 'avoid', 'all', 'avoid-page', 'page', 'left', 'right', 'column'] as const
     const scalePosition = () =>
         [
-            'bottom',
             'center',
+            'top',
+            'bottom',
             'left',
-            'left-bottom',
-            'left-top',
             'right',
-            'right-bottom',
+            'top-left',
+            // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378
+            'left-top',
+            'top-right',
+            // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378
             'right-top',
-            'top',
+            'bottom-right',
+            // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378
+            'right-bottom',
+            'bottom-left',
+            // Deprecated since Tailwind CSS v4.1.0, see https://github.com/tailwindlabs/tailwindcss/pull/17378
+            'left-bottom',
         ] as const
+    const scalePositionWithArbitrary = () =>
+        [...scalePosition(), isArbitraryVariable, isArbitraryValue] as const
     const scaleOverflow = () => ['auto', 'hidden', 'clip', 'visible', 'scroll'] as const
     const scaleOverscroll = () => ['auto', 'contain', 'none'] as const
     const scaleUnambiguousSpacing = () =>
@@ -92,8 +103,20 @@ export const getDefaultConfig = () => {
     const scaleGridAutoColsRows = () =>
         ['auto', 'min', 'max', 'fr', isArbitraryVariable, isArbitraryValue] as const
     const scaleAlignPrimaryAxis = () =>
-        ['start', 'end', 'center', 'between', 'around', 'evenly', 'stretch', 'baseline'] as const
-    const scaleAlignSecondaryAxis = () => ['start', 'end', 'center', 'stretch'] as const
+        [
+            'start',
+            'end',
+            'center',
+            'between',
+            'around',
+            'evenly',
+            'stretch',
+            'baseline',
+            'center-safe',
+            'end-safe',
+        ] as const
+    const scaleAlignSecondaryAxis = () =>
+        ['start', 'end', 'center', 'stretch', 'center-safe', 'end-safe'] as const
     const scaleMargin = () => ['auto', ...scaleUnambiguousSpacing()] as const
     const scaleSizing = () =>
         [
@@ -112,6 +135,23 @@ export const getDefaultConfig = () => {
             ...scaleUnambiguousSpacing(),
         ] as const
     const scaleColor = () => [themeColor, isArbitraryVariable, isArbitraryValue] as const
+    const scaleBgPosition = () =>
+        [
+            ...scalePosition(),
+            isArbitraryVariablePosition,
+            isArbitraryPosition,
+            { position: [isArbitraryVariable, isArbitraryValue] },
+        ] as const
+    const scaleBgRepeat = () => ['no-repeat', { repeat: ['', 'x', 'y', 'space', 'round'] }] as const
+    const scaleBgSize = () =>
+        [
+            'auto',
+            'cover',
+            'contain',
+            isArbitraryVariableSize,
+            isArbitrarySize,
+            { size: [isArbitraryVariable, isArbitraryValue] },
+        ] as const
     const scaleGradientStopPosition = () =>
         [isPercent, isArbitraryVariableLength, isArbitraryLength] as const
     const scaleRadius = () =>
@@ -146,6 +186,8 @@ export const getDefaultConfig = () => {
             'color',
             'luminosity',
         ] as const
+    const scaleMaskImagePosition = () =>
+        [isNumber, isPercent, isArbitraryVariablePosition, isArbitraryPosition] as const
     const scaleBlur = () =>
         [
             // Deprecated since Tailwind CSS v4.0.0
@@ -155,20 +197,6 @@ export const getDefaultConfig = () => {
             isArbitraryVariable,
             isArbitraryValue,
         ] as const
-    const scaleOrigin = () =>
-        [
-            'center',
-            'top',
-            'top-right',
-            'right',
-            'bottom-right',
-            'bottom',
-            'bottom-left',
-            'left',
-            'top-left',
-            isArbitraryVariable,
-            isArbitraryValue,
-        ] as const
     const scaleRotate = () => ['none', isNumber, isArbitraryVariable, isArbitraryValue] as const
     const scaleScale = () => ['none', isNumber, isArbitraryVariable, isArbitraryValue] as const
     const scaleSkew = () => [isNumber, isArbitraryVariable, isArbitraryValue] as const
@@ -204,6 +232,7 @@ export const getDefaultConfig = () => {
             shadow: [isTshirtSize],
             spacing: ['px', isNumber],
             text: [isTshirtSize],
+            'text-shadow': [isTshirtSize],
             tracking: ['tighter', 'tight', 'normal', 'wide', 'wider', 'widest'],
         },
         classGroups: {
@@ -321,9 +350,7 @@ export const getDefaultConfig = () => {
              * Object Position
              * @see https://tailwindcss.com/docs/object-position
              */
-            'object-position': [
-                { object: [...scalePosition(), isArbitraryValue, isArbitraryVariable] },
-            ],
+            'object-position': [{ object: scalePositionWithArbitrary() }],
             /**
              * Overflow
              * @see https://tailwindcss.com/docs/overflow
@@ -569,12 +596,14 @@ export const getDefaultConfig = () => {
              * Align Items
              * @see https://tailwindcss.com/docs/align-items
              */
-            'align-items': [{ items: [...scaleAlignSecondaryAxis(), 'baseline'] }],
+            'align-items': [{ items: [...scaleAlignSecondaryAxis(), { baseline: ['', 'last'] }] }],
             /**
              * Align Self
              * @see https://tailwindcss.com/docs/align-self
              */
-            'align-self': [{ self: ['auto', ...scaleAlignSecondaryAxis(), 'baseline'] }],
+            'align-self': [
+                { self: ['auto', ...scaleAlignSecondaryAxis(), { baseline: ['', 'last'] }] },
+            ],
             /**
              * Place Content
              * @see https://tailwindcss.com/docs/place-content
@@ -994,6 +1023,11 @@ export const getDefaultConfig = () => {
              * @see https://tailwindcss.com/docs/word-break
              */
             break: [{ break: ['normal', 'words', 'all', 'keep'] }],
+            /**
+             * Overflow Wrap
+             * @see https://tailwindcss.com/docs/overflow-wrap
+             */
+            wrap: [{ wrap: ['break-word', 'anywhere', 'normal'] }],
             /**
              * Hyphens
              * @see https://tailwindcss.com/docs/hyphens
@@ -1028,21 +1062,17 @@ export const getDefaultConfig = () => {
              * Background Position
              * @see https://tailwindcss.com/docs/background-position
              */
-            'bg-position': [
-                { bg: [...scalePosition(), isArbitraryVariablePosition, isArbitraryPosition] },
-            ],
+            'bg-position': [{ bg: scaleBgPosition() }],
             /**
              * Background Repeat
              * @see https://tailwindcss.com/docs/background-repeat
              */
-            'bg-repeat': [{ bg: ['no-repeat', { repeat: ['', 'x', 'y', 'space', 'round'] }] }],
+            'bg-repeat': [{ bg: scaleBgRepeat() }],
             /**
              * Background Size
              * @see https://tailwindcss.com/docs/background-size
              */
-            'bg-size': [
-                { bg: ['auto', 'cover', 'contain', isArbitraryVariableSize, isArbitrarySize] },
-            ],
+            'bg-size': [{ bg: scaleBgSize() }],
             /**
              * Background Image
              * @see https://tailwindcss.com/docs/background-image
@@ -1329,7 +1359,7 @@ export const getDefaultConfig = () => {
              * Outline Color
              * @see https://tailwindcss.com/docs/outline-color
              */
-            'outline-color': [{ outline: [themeColor] }],
+            'outline-color': [{ outline: scaleColor() }],
 
             // ---------------
             // --- Effects ---
@@ -1364,9 +1394,9 @@ export const getDefaultConfig = () => {
                 {
                     'inset-shadow': [
                         'none',
-                        isArbitraryVariable,
-                        isArbitraryValue,
                         themeInsetShadow,
+                        isArbitraryVariableShadow,
+                        isArbitraryShadow,
                     ],
                 },
             ],
@@ -1416,6 +1446,25 @@ export const getDefaultConfig = () => {
              * @see https://tailwindcss.com/docs/box-shadow#setting-the-inset-ring-color
              */
             'inset-ring-color': [{ 'inset-ring': scaleColor() }],
+            /**
+             * Text Shadow
+             * @see https://tailwindcss.com/docs/text-shadow
+             */
+            'text-shadow': [
+                {
+                    'text-shadow': [
+                        'none',
+                        themeTextShadow,
+                        isArbitraryVariableShadow,
+                        isArbitraryShadow,
+                    ],
+                },
+            ],
+            /**
+             * Text Shadow Color
+             * @see https://tailwindcss.com/docs/text-shadow#setting-the-shadow-color
+             */
+            'text-shadow-color': [{ 'text-shadow': scaleColor() }],
             /**
              * Opacity
              * @see https://tailwindcss.com/docs/opacity
@@ -1431,6 +1480,104 @@ export const getDefaultConfig = () => {
              * @see https://tailwindcss.com/docs/background-blend-mode
              */
             'bg-blend': [{ 'bg-blend': scaleBlendMode() }],
+            /**
+             * Mask Clip
+             * @see https://tailwindcss.com/docs/mask-clip
+             */
+            'mask-clip': [
+                { 'mask-clip': ['border', 'padding', 'content', 'fill', 'stroke', 'view'] },
+                'mask-no-clip',
+            ],
+            /**
+             * Mask Composite
+             * @see https://tailwindcss.com/docs/mask-composite
+             */
+            'mask-composite': [{ mask: ['add', 'subtract', 'intersect', 'exclude'] }],
+            /**
+             * Mask Image
+             * @see https://tailwindcss.com/docs/mask-image
+             */
+            'mask-image-linear-pos': [{ 'mask-linear': [isNumber] }],
+            'mask-image-linear-from-pos': [{ 'mask-linear-from': scaleMaskImagePosition() }],
+            'mask-image-linear-to-pos': [{ 'mask-linear-to': scaleMaskImagePosition() }],
+            'mask-image-linear-from-color': [{ 'mask-linear-from': scaleColor() }],
+            'mask-image-linear-to-color': [{ 'mask-linear-to': scaleColor() }],
+            'mask-image-t-from-pos': [{ 'mask-t-from': scaleMaskImagePosition() }],
+            'mask-image-t-to-pos': [{ 'mask-t-to': scaleMaskImagePosition() }],
+            'mask-image-t-from-color': [{ 'mask-t-from': scaleColor() }],
+            'mask-image-t-to-color': [{ 'mask-t-to': scaleColor() }],
+            'mask-image-r-from-pos': [{ 'mask-r-from': scaleMaskImagePosition() }],
+            'mask-image-r-to-pos': [{ 'mask-r-to': scaleMaskImagePosition() }],
+            'mask-image-r-from-color': [{ 'mask-r-from': scaleColor() }],
+            'mask-image-r-to-color': [{ 'mask-r-to': scaleColor() }],
+            'mask-image-b-from-pos': [{ 'mask-b-from': scaleMaskImagePosition() }],
+            'mask-image-b-to-pos': [{ 'mask-b-to': scaleMaskImagePosition() }],
+            'mask-image-b-from-color': [{ 'mask-b-from': scaleColor() }],
+            'mask-image-b-to-color': [{ 'mask-b-to': scaleColor() }],
+            'mask-image-l-from-pos': [{ 'mask-l-from': scaleMaskImagePosition() }],
+            'mask-image-l-to-pos': [{ 'mask-l-to': scaleMaskImagePosition() }],
+            'mask-image-l-from-color': [{ 'mask-l-from': scaleColor() }],
+            'mask-image-l-to-color': [{ 'mask-l-to': scaleColor() }],
+            'mask-image-x-from-pos': [{ 'mask-x-from': scaleMaskImagePosition() }],
+            'mask-image-x-to-pos': [{ 'mask-x-to': scaleMaskImagePosition() }],
+            'mask-image-x-from-color': [{ 'mask-x-from': scaleColor() }],
+            'mask-image-x-to-color': [{ 'mask-x-to': scaleColor() }],
+            'mask-image-y-from-pos': [{ 'mask-y-from': scaleMaskImagePosition() }],
+            'mask-image-y-to-pos': [{ 'mask-y-to': scaleMaskImagePosition() }],
+            'mask-image-y-from-color': [{ 'mask-y-from': scaleColor() }],
+            'mask-image-y-to-color': [{ 'mask-y-to': scaleColor() }],
+            'mask-image-radial': [{ 'mask-radial': [isArbitraryVariable, isArbitraryValue] }],
+            'mask-image-radial-from-pos': [{ 'mask-radial-from': scaleMaskImagePosition() }],
+            'mask-image-radial-to-pos': [{ 'mask-radial-to': scaleMaskImagePosition() }],
+            'mask-image-radial-from-color': [{ 'mask-radial-from': scaleColor() }],
+            'mask-image-radial-to-color': [{ 'mask-radial-to': scaleColor() }],
+            'mask-image-radial-shape': [{ 'mask-radial': ['circle', 'ellipse'] }],
+            'mask-image-radial-size': [
+                { 'mask-radial': [{ closest: ['side', 'corner'], farthest: ['side', 'corner'] }] },
+            ],
+            'mask-image-radial-pos': [{ 'mask-radial-at': scalePosition() }],
+            'mask-image-conic-pos': [{ 'mask-conic': [isNumber] }],
+            'mask-image-conic-from-pos': [{ 'mask-conic-from': scaleMaskImagePosition() }],
+            'mask-image-conic-to-pos': [{ 'mask-conic-to': scaleMaskImagePosition() }],
+            'mask-image-conic-from-color': [{ 'mask-conic-from': scaleColor() }],
+            'mask-image-conic-to-color': [{ 'mask-conic-to': scaleColor() }],
+            /**
+             * Mask Mode
+             * @see https://tailwindcss.com/docs/mask-mode
+             */
+            'mask-mode': [{ mask: ['alpha', 'luminance', 'match'] }],
+            /**
+             * Mask Origin
+             * @see https://tailwindcss.com/docs/mask-origin
+             */
+            'mask-origin': [
+                { 'mask-origin': ['border', 'padding', 'content', 'fill', 'stroke', 'view'] },
+            ],
+            /**
+             * Mask Position
+             * @see https://tailwindcss.com/docs/mask-position
+             */
+            'mask-position': [{ mask: scaleBgPosition() }],
+            /**
+             * Mask Repeat
+             * @see https://tailwindcss.com/docs/mask-repeat
+             */
+            'mask-repeat': [{ mask: scaleBgRepeat() }],
+            /**
+             * Mask Size
+             * @see https://tailwindcss.com/docs/mask-size
+             */
+            'mask-size': [{ mask: scaleBgSize() }],
+            /**
+             * Mask Type
+             * @see https://tailwindcss.com/docs/mask-type
+             */
+            'mask-type': [{ 'mask-type': ['alpha', 'luminance'] }],
+            /**
+             * Mask Image
+             * @see https://tailwindcss.com/docs/mask-image
+             */
+            'mask-image': [{ mask: ['none', isArbitraryVariable, isArbitraryValue] }],
 
             // ---------------
             // --- Filters ---
@@ -1477,11 +1624,16 @@ export const getDefaultConfig = () => {
                         '',
                         'none',
                         themeDropShadow,
-                        isArbitraryVariable,
-                        isArbitraryValue,
+                        isArbitraryVariableShadow,
+                        isArbitraryShadow,
                     ],
                 },
             ],
+            /**
+             * Drop Shadow Color
+             * @see https://tailwindcss.com/docs/filter-drop-shadow#setting-the-shadow-color
+             */
+            'drop-shadow-color': [{ 'drop-shadow': scaleColor() }],
             /**
              * Grayscale
              * @see https://tailwindcss.com/docs/grayscale
@@ -1690,7 +1842,7 @@ export const getDefaultConfig = () => {
              * Perspective Origin
              * @see https://tailwindcss.com/docs/perspective-origin
              */
-            'perspective-origin': [{ 'perspective-origin': scaleOrigin() }],
+            'perspective-origin': [{ 'perspective-origin': scalePositionWithArbitrary() }],
             /**
              * Rotate
              * @see https://tailwindcss.com/docs/rotate
@@ -1762,7 +1914,7 @@ export const getDefaultConfig = () => {
              * Transform Origin
              * @see https://tailwindcss.com/docs/transform-origin
              */
-            'transform-origin': [{ origin: scaleOrigin() }],
+            'transform-origin': [{ origin: scalePositionWithArbitrary() }],
             /**
              * Transform Style
              * @see https://tailwindcss.com/docs/transform-style
@@ -2132,6 +2284,8 @@ export const getDefaultConfig = () => {
             'rounded-l': ['rounded-tl', 'rounded-bl'],
             'border-spacing': ['border-spacing-x', 'border-spacing-y'],
             'border-w': [
+                'border-w-x',
+                'border-w-y',
                 'border-w-s',
                 'border-w-e',
                 'border-w-t',
@@ -2142,6 +2296,8 @@ export const getDefaultConfig = () => {
             'border-w-x': ['border-w-r', 'border-w-l'],
             'border-w-y': ['border-w-t', 'border-w-b'],
             'border-color': [
+                'border-color-x',
+                'border-color-y',
                 'border-color-s',
                 'border-color-e',
                 'border-color-t',
@@ -2186,17 +2342,18 @@ export const getDefaultConfig = () => {
             'font-size': ['leading'],
         },
         orderSensitiveModifiers: [
-            'before',
+            '*',
+            '**',
             'after',
-            'placeholder',
+            'backdrop',
+            'before',
+            'details-content',
             'file',
+            'first-letter',
+            'first-line',
             'marker',
+            'placeholder',
             'selection',
-            'first-line',
-            'first-letter',
-            'backdrop',
-            '*',
-            '**',
         ],
     } as const satisfies Config<DefaultClassGroupIds, DefaultThemeGroupIds>
 }
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 3f6c31d..274c95a 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -107,8 +107,10 @@ interface ConfigGroupsPart<ClassGroupIds extends string, ThemeGroupIds extends s
     /**
      * Conflicting classes across groups.
      *
-     * The key is ID of class group which creates conflict, values are IDs of class groups which receive a conflict.
-     * A class group ID is the key of a class group in classGroups object.
+     * The key is the ID of a class group which creates a conflict, values are IDs of class groups which receive a conflict. That means if a class from from the key ID is present, all preceding classes from the values are removed.
+     *
+     * A class group ID is the key of a class group in the classGroups object.
+     *
      * @example { gap: ['gap-x', 'gap-y'] }
      */
     conflictingClassGroups: NoInfer<Partial<Record<ClassGroupIds, readonly ClassGroupIds[]>>>
@@ -116,6 +118,7 @@ interface ConfigGroupsPart<ClassGroupIds extends string, ThemeGroupIds extends s
      * Postfix modifiers conflicting with other class groups.
      *
      * A class group ID is the key of a class group in classGroups object.
+     *
      * @example { 'font-size': ['leading'] }
      */
     conflictingClassGroupModifiers: NoInfer<
@@ -196,6 +199,7 @@ export type DefaultThemeGroupIds =
     | 'shadow'
     | 'spacing'
     | 'text'
+    | 'text-shadow'
     | 'tracking'
 
 /**
@@ -285,6 +289,7 @@ export type DefaultClassGroupIds =
     | 'divide-y-reverse'
     | 'divide-y'
     | 'drop-shadow'
+    | 'drop-shadow-color'
     | 'duration'
     | 'ease'
     | 'end'
@@ -345,6 +350,57 @@ export type DefaultClassGroupIds =
     | 'list-style-position'
     | 'list-style-type'
     | 'm'
+    | 'mask-clip'
+    | 'mask-composite'
+    | 'mask-image-b-from-color'
+    | 'mask-image-b-from-pos'
+    | 'mask-image-b-to-color'
+    | 'mask-image-b-to-pos'
+    | 'mask-image-conic-from-color'
+    | 'mask-image-conic-from-pos'
+    | 'mask-image-conic-pos'
+    | 'mask-image-conic-to-color'
+    | 'mask-image-conic-to-pos'
+    | 'mask-image-l-from-color'
+    | 'mask-image-l-from-pos'
+    | 'mask-image-l-to-color'
+    | 'mask-image-l-to-pos'
+    | 'mask-image-linear-from-color'
+    | 'mask-image-linear-from-pos'
+    | 'mask-image-linear-pos'
+    | 'mask-image-linear-to-color'
+    | 'mask-image-linear-to-pos'
+    | 'mask-image-r-from-color'
+    | 'mask-image-r-from-pos'
+    | 'mask-image-r-to-color'
+    | 'mask-image-r-to-pos'
+    | 'mask-image-radial-from-color'
+    | 'mask-image-radial-from-pos'
+    | 'mask-image-radial-pos'
+    | 'mask-image-radial-shape'
+    | 'mask-image-radial-size'
+    | 'mask-image-radial-to-color'
+    | 'mask-image-radial-to-pos'
+    | 'mask-image-radial'
+    | 'mask-image-t-from-color'
+    | 'mask-image-t-from-pos'
+    | 'mask-image-t-to-color'
+    | 'mask-image-t-to-pos'
+    | 'mask-image-x-from-color'
+    | 'mask-image-x-from-pos'
+    | 'mask-image-x-to-color'
+    | 'mask-image-x-to-pos'
+    | 'mask-image-y-from-color'
+    | 'mask-image-y-from-pos'
+    | 'mask-image-y-to-color'
+    | 'mask-image-y-to-pos'
+    | 'mask-image'
+    | 'mask-mode'
+    | 'mask-origin'
+    | 'mask-position'
+    | 'mask-repeat'
+    | 'mask-size'
+    | 'mask-type'
     | 'max-h'
     | 'max-w'
     | 'mb'
@@ -472,6 +528,8 @@ export type DefaultClassGroupIds =
     | 'text-decoration-thickness'
     | 'text-decoration'
     | 'text-overflow'
+    | 'text-shadow'
+    | 'text-shadow-color'
     | 'text-transform'
     | 'text-wrap'
     | 'top'
@@ -496,6 +554,7 @@ export type DefaultClassGroupIds =
     | 'w'
     | 'whitespace'
     | 'will-change'
+    | 'wrap'
     | 'z'
 
 export type AnyClassGroupIds = string
diff --git a/src/lib/validators.ts b/src/lib/validators.ts
index 5e9b427..28c9d53 100644
--- a/src/lib/validators.ts
+++ b/src/lib/validators.ts
@@ -12,9 +12,9 @@ const imageRegex =
 
 export const isFraction = (value: string) => fractionRegex.test(value)
 
-export const isNumber = (value: string) => Boolean(value) && !Number.isNaN(Number(value))
+export const isNumber = (value: string) => !!value && !Number.isNaN(Number(value))
 
-export const isInteger = (value: string) => Boolean(value) && Number.isInteger(Number(value))
+export const isInteger = (value: string) => !!value && Number.isInteger(Number(value))
 
 export const isPercent = (value: string) => value.endsWith('%') && isNumber(value.slice(0, -1))
 
@@ -52,7 +52,8 @@ export const isArbitraryPosition = (value: string) =>
 
 export const isArbitraryImage = (value: string) => getIsArbitraryValue(value, isLabelImage, isImage)
 
-export const isArbitraryShadow = (value: string) => getIsArbitraryValue(value, isNever, isShadow)
+export const isArbitraryShadow = (value: string) =>
+    getIsArbitraryValue(value, isLabelShadow, isShadow)
 
 export const isArbitraryVariable = (value: string) => arbitraryVariableRegex.test(value)
 
@@ -112,15 +113,11 @@ const getIsArbitraryVariable = (
 
 // Labels
 
-const isLabelPosition = (label: string) => label === 'position'
+const isLabelPosition = (label: string) => label === 'position' || label === 'percentage'
 
-const imageLabels = new Set(['image', 'url'])
+const isLabelImage = (label: string) => label === 'image' || label === 'url'
 
-const isLabelImage = (label: string) => imageLabels.has(label)
-
-const sizeLabels = new Set(['length', 'size', 'percentage'])
-
-const isLabelSize = (label: string) => sizeLabels.has(label)
+const isLabelSize = (label: string) => label === 'length' || label === 'size' || label === 'bg-size'
 
 const isLabelLength = (label: string) => label === 'length'
 


Test patch:
diff --git a/tests/arbitrary-values.test.ts b/tests/arbitrary-values.test.ts
index 9f8e081..6a4d047 100644
--- a/tests/arbitrary-values.test.ts
+++ b/tests/arbitrary-values.test.ts
@@ -67,9 +67,9 @@ test('handles ambiguous arbitrary values correctly', () => {
     expect(twMerge('text-2xl text-[calc(theme(fontSize.4xl)/1.125)]')).toBe(
         'text-[calc(theme(fontSize.4xl)/1.125)]',
     )
-    expect(twMerge('bg-cover bg-[percentage:30%] bg-[length:200px_100px]')).toBe(
-        'bg-[length:200px_100px]',
-    )
+    expect(
+        twMerge('bg-cover bg-[percentage:30%] bg-[size:200px_100px] bg-[length:200px_100px]'),
+    ).toBe('bg-[percentage:30%] bg-[length:200px_100px]')
     expect(
         twMerge(
             'bg-none bg-[url(.)] bg-[image:.] bg-[url:.] bg-[linear-gradient(.)] bg-linear-to-r',
diff --git a/tests/class-map.test.ts b/tests/class-map.test.ts
index 7d81fae..2e3e99e 100644
--- a/tests/class-map.test.ts
+++ b/tests/class-map.test.ts
@@ -103,7 +103,7 @@ test('class map has correct class groups at first part', () => {
             'divide-y',
             'divide-y-reverse',
         ],
-        drop: ['drop-shadow'],
+        drop: ['drop-shadow', 'drop-shadow-color'],
         duration: ['duration'],
         ease: ['ease'],
         end: ['end'],
@@ -150,6 +150,59 @@ test('class map has correct class groups at first part', () => {
         list: ['display', 'list-image', 'list-style-position', 'list-style-type'],
         lowercase: ['text-transform'],
         m: ['m'],
+        mask: [
+            'mask-clip',
+            'mask-composite',
+            'mask-image',
+            'mask-image-b-from-color',
+            'mask-image-b-from-pos',
+            'mask-image-b-to-color',
+            'mask-image-b-to-pos',
+            'mask-image-conic-from-color',
+            'mask-image-conic-from-pos',
+            'mask-image-conic-pos',
+            'mask-image-conic-to-color',
+            'mask-image-conic-to-pos',
+            'mask-image-l-from-color',
+            'mask-image-l-from-pos',
+            'mask-image-l-to-color',
+            'mask-image-l-to-pos',
+            'mask-image-linear-from-color',
+            'mask-image-linear-from-pos',
+            'mask-image-linear-pos',
+            'mask-image-linear-to-color',
+            'mask-image-linear-to-pos',
+            'mask-image-r-from-color',
+            'mask-image-r-from-pos',
+            'mask-image-r-to-color',
+            'mask-image-r-to-pos',
+            'mask-image-radial',
+            'mask-image-radial-from-color',
+            'mask-image-radial-from-pos',
+            'mask-image-radial-pos',
+            'mask-image-radial-shape',
+            'mask-image-radial-size',
+            'mask-image-radial-to-color',
+            'mask-image-radial-to-pos',
+            'mask-image-t-from-color',
+            'mask-image-t-from-pos',
+            'mask-image-t-to-color',
+            'mask-image-t-to-pos',
+            'mask-image-x-from-color',
+            'mask-image-x-from-pos',
+            'mask-image-x-to-color',
+            'mask-image-x-to-pos',
+            'mask-image-y-from-color',
+            'mask-image-y-from-pos',
+            'mask-image-y-to-color',
+            'mask-image-y-to-pos',
+            'mask-mode',
+            'mask-origin',
+            'mask-position',
+            'mask-repeat',
+            'mask-size',
+            'mask-type',
+        ],
         max: ['max-h', 'max-w'],
         mb: ['mb'],
         me: ['me'],
@@ -254,7 +307,15 @@ test('class map has correct class groups at first part', () => {
         subpixel: ['font-smoothing'],
         table: ['display', 'table-layout'],
         tabular: ['fvn-spacing'],
-        text: ['font-size', 'text-alignment', 'text-color', 'text-overflow', 'text-wrap'],
+        text: [
+            'font-size',
+            'text-alignment',
+            'text-color',
+            'text-overflow',
+            'text-shadow',
+            'text-shadow-color',
+            'text-wrap',
+        ],
         to: ['gradient-to', 'gradient-to-pos'],
         top: ['top'],
         touch: ['touch', 'touch-pz', 'touch-x', 'touch-y'],
@@ -270,6 +331,7 @@ test('class map has correct class groups at first part', () => {
         w: ['w'],
         whitespace: ['whitespace'],
         will: ['will-change'],
+        wrap: ['wrap'],
         z: ['z'],
     })
 })
diff --git a/tests/tailwind-css-versions.test.ts b/tests/tailwind-css-versions.test.ts
index 9c47b4d..41fbcec 100644
--- a/tests/tailwind-css-versions.test.ts
+++ b/tests/tailwind-css-versions.test.ts
@@ -87,3 +87,77 @@ test('supports Tailwind CSS v4.0 features', () => {
         'via-red-500 via-(length:--mobile-header-gradient)',
     )
 })
+
+test('supports Tailwind CSS v4.1 features', () => {
+    expect(twMerge('items-baseline items-baseline-last')).toBe('items-baseline-last')
+    expect(twMerge('self-baseline self-baseline-last')).toBe('self-baseline-last')
+    expect(twMerge('place-content-center place-content-end-safe place-content-center-safe')).toBe(
+        'place-content-center-safe',
+    )
+    expect(twMerge('items-center-safe items-baseline items-end-safe')).toBe('items-end-safe')
+    expect(twMerge('wrap-break-word wrap-normal wrap-anywhere')).toBe('wrap-anywhere')
+    expect(twMerge('text-shadow-none text-shadow-2xl')).toBe('text-shadow-2xl')
+    expect(
+        twMerge(
+            'text-shadow-none text-shadow-md text-shadow-red text-shadow-red-500 shadow-red shadow-3xs',
+        ),
+    ).toBe('text-shadow-md text-shadow-red-500 shadow-red shadow-3xs')
+    expect(twMerge('mask-add mask-subtract')).toBe('mask-subtract')
+    expect(
+        twMerge(
+            // mask-image
+            'mask-(--foo) mask-[foo] mask-none',
+            // mask-image-linear-pos
+            'mask-linear-1 mask-linear-2',
+            // mask-image-linear-from-pos
+            'mask-linear-from-[position:test] mask-linear-from-3',
+            // mask-image-linear-to-pos
+            'mask-linear-to-[position:test] mask-linear-to-3',
+            // mask-image-linear-from-color
+            'mask-linear-from-color-red mask-linear-from-color-3',
+            // mask-image-linear-to-color
+            'mask-linear-to-color-red mask-linear-to-color-3',
+            // mask-image-t-from-pos
+            'mask-t-from-[position:test] mask-t-from-3',
+            // mask-image-t-to-pos
+            'mask-t-to-[position:test] mask-t-to-3',
+            // mask-image-t-from-color
+            'mask-t-from-color-red mask-t-from-color-3',
+            // mask-image-radial
+            'mask-radial-(--test) mask-radial-[test]',
+            // mask-image-radial-from-pos
+            'mask-radial-from-[position:test] mask-radial-from-3',
+            // mask-image-radial-to-pos
+            'mask-radial-to-[position:test] mask-radial-to-3',
+            // mask-image-radial-from-color
+            'mask-radial-from-color-red mask-radial-from-color-3',
+        ),
+    ).toBe(
+        'mask-none mask-linear-2 mask-linear-from-3 mask-linear-to-3 mask-linear-from-color-3 mask-linear-to-color-3 mask-t-from-3 mask-t-to-3 mask-t-from-color-3 mask-radial-[test] mask-radial-from-3 mask-radial-to-3 mask-radial-from-color-3',
+    )
+    expect(
+        twMerge(
+            // mask-image
+            'mask-(--something) mask-[something]',
+            // mask-position
+            'mask-top-left mask-center mask-(position:--var) mask-[position:1px_1px] mask-position-(--var) mask-position-[1px_1px]',
+        ),
+    ).toBe('mask-[something] mask-position-[1px_1px]')
+    expect(
+        twMerge(
+            // mask-image
+            'mask-(--something) mask-[something]',
+            // mask-size
+            'mask-auto mask-[size:foo] mask-(size:--foo) mask-size-[foo] mask-size-(--foo) mask-cover mask-contain',
+        ),
+    ).toBe('mask-[something] mask-contain')
+    expect(twMerge('mask-type-luminance mask-type-alpha')).toBe('mask-type-alpha')
+    expect(twMerge('shadow-md shadow-lg/25 text-shadow-md text-shadow-lg/25')).toBe(
+        'shadow-lg/25 text-shadow-lg/25',
+    )
+    expect(
+        twMerge('drop-shadow-some-color drop-shadow-[#123456] drop-shadow-lg drop-shadow-[10px_0]'),
+    ).toBe('drop-shadow-[#123456] drop-shadow-[10px_0]')
+    expect(twMerge('drop-shadow-[#123456] drop-shadow-some-color')).toBe('drop-shadow-some-color')
+    expect(twMerge('drop-shadow-2xl drop-shadow-[shadow:foo]')).toBe('drop-shadow-[shadow:foo]')
+})
diff --git a/tests/validators.test.ts b/tests/validators.test.ts
index 9fdd96d..b123aac 100644
--- a/tests/validators.test.ts
+++ b/tests/validators.test.ts
@@ -95,6 +95,7 @@ test('isArbitraryNumber', () => {
 test('isArbitraryPosition', () => {
     expect(isArbitraryPosition('[position:2px]')).toBe(true)
     expect(isArbitraryPosition('[position:bla]')).toBe(true)
+    expect(isArbitraryPosition('[percentage:bla]')).toBe(true)
 
     expect(isArbitraryPosition('[2px]')).toBe(false)
     expect(isArbitraryPosition('[bla]')).toBe(false)
@@ -120,11 +121,11 @@ test('isArbitrarySize', () => {
     expect(isArbitrarySize('[size:2px]')).toBe(true)
     expect(isArbitrarySize('[size:bla]')).toBe(true)
     expect(isArbitrarySize('[length:bla]')).toBe(true)
-    expect(isArbitrarySize('[percentage:bla]')).toBe(true)
 
     expect(isArbitrarySize('[2px]')).toBe(false)
     expect(isArbitrarySize('[bla]')).toBe(false)
     expect(isArbitrarySize('size:2px')).toBe(false)
+    expect(isArbitrarySize('[percentage:bla]')).toBe(false)
 })
 
 test('isArbitraryValue', () => {
@@ -187,6 +188,7 @@ test('isArbitraryVariablePosition', () => {
     expect(isArbitraryVariablePosition('(other:test)')).toBe(false)
     expect(isArbitraryVariablePosition('(test)')).toBe(false)
     expect(isArbitraryVariablePosition('position:test')).toBe(false)
+    expect(isArbitraryVariablePosition('percentage:test')).toBe(false)
 })
 
 test('isArbitraryVariableShadow', () => {
@@ -200,11 +202,11 @@ test('isArbitraryVariableShadow', () => {
 test('isArbitraryVariableSize', () => {
     expect(isArbitraryVariableSize('(size:test)')).toBe(true)
     expect(isArbitraryVariableSize('(length:test)')).toBe(true)
-    expect(isArbitraryVariableSize('(percentage:test)')).toBe(true)
 
     expect(isArbitraryVariableSize('(other:test)')).toBe(false)
     expect(isArbitraryVariableSize('(test)')).toBe(false)
     expect(isArbitraryVariableSize('size:test')).toBe(false)
+    expect(isArbitraryVariableSize('(percentage:test)')).toBe(false)
 })
 
 test('isFraction', () => {