Fix negation of theme() in arbitrary values (#19928)

## Problem

Using a negative modifier with `theme()` inside an arbitrary value
produces no CSS output.

- ✅ `ms-[theme(margin.1)]` — works
- ✅ `-ms-[10px]` — works
- ❌ `-ms-[theme(margin.1)]` — no output

## Cause

`negateValue()` in `src/util/negateValue.js` doesn't recognize `theme()`
as a negatable function. It falls through all conditions and returns
`undefined`, causing the utility to be silently dropped.

## Fix

Added `'theme'` to the `numericFunctions` list so `theme()` values get
wrapped in `calc(... * -1)`, which the PostCSS `theme()` resolver then
processes downstream.

## Tests

- **Unit test** (`negateValue.test.js`):
`negateValue('theme(margin.1)')` returns `calc(theme(margin.1) * -1)`
- **Integration test** (`arbitrary-values.test.js`):
`-mt-[theme(spacing.1)]` produces `margin-top: -0.25rem`

---------

Co-authored-by: Casey Corcoran <casecorc@amazon.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Casey Corcoran 2026-08-03 09:11:32 -05:00 • committed by GitHub
parent ff8b2a0db9
commit 7a836d8495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 31 additions and 5 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Ensure classes are extracted when a variant precedes an arbitrary variant containing a quoted attribute selector (e.g. `focus-visible:[&:not([aria-selected="true"])]:bg-red-500`) ([#20374](https://github.com/tailwindlabs/tailwindcss/pull/20374))
- Fix negation of `theme()` in arbitrary values (e.g. `-m-[theme(margin.1)]`) ([#19928](https://github.com/tailwindlabs/tailwindcss/pull/19928))
## [3.4.19] - 2025-12-10

View file

@ -10,11 +10,13 @@ export default function negateValue(value) {
return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
}
// What functions we support negating numeric values for
// var() isn't inherently a numeric function but we support it anyway
// The trigonometric functions are omitted because you'll need to use calc(…) with them _anyway_
// to produce generally useful results and that will be covered already
let numericFunctions = ['var', 'calc', 'min', 'max', 'clamp']
// What functions we support negating numeric values for var() and theme()
// aren't inherently a numeric function but we support it anyway
//
// The trigonometric functions are omitted because you'll need to use calc(…)
// with them _anyway_ to produce generally useful results and that will be
// covered already
let numericFunctions = ['var', 'theme', 'calc', 'min', 'max', 'clamp']
for (const fn of numericFunctions) {
if (value.includes(`${fn}(`)) {

View file

@ -354,6 +354,25 @@ it('should be possible to read theme values in arbitrary values (without quotes)
})
})
it('should be possible to negate theme values in arbitrary values with negative modifier', () => {
let config = {
content: [{ raw: html`<div class="-mt-[theme(spacing.1)]"></div>` }],
theme: {
spacing: {
1: '0.25rem',
},
},
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.-mt-\[theme\(spacing\.1\)\] {
margin-top: -0.25rem;
}
`)
})
})
it('should be possible to read theme values in arbitrary values (with quotes)', () => {
let config = {
content: [{ raw: html`<div class="w-[theme('spacing.1')] w-[theme('spacing[0.5]')]"></div>` }],

View file

@ -12,3 +12,7 @@ test('values that cannot be negated become undefined', () => {
expect(negateValue('auto')).toBeUndefined()
expect(negateValue('cover')).toBeUndefined()
})
test('it negates theme() function calls', () => {
expect(negateValue('theme(margin.1)')).toEqual('calc(theme(margin.1) * -1)')
})