diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b7eab50d4f9..2467d850eee6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
-- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
+- _Experimental_: Add template codemods for removal of automatic `var(…)` injection ([#14526](https://github.com/tailwindlabs/tailwindcss/pull/14526))
+- _Experimental_: Add template codemods for migrating `bg-gradient-*` utilities to `bg-linear-*` ([#14537](https://github.com/tailwindlabs/tailwindcss/pull/14537]))
+- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
### Fixed
@@ -39,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: Add CSS codemods for `@apply` ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14434))
- _Experimental_: Add CSS codemods for migrating `@tailwind` directives ([#14411](https://github.com/tailwindlabs/tailwindcss/pull/14411), [#14504](https://github.com/tailwindlabs/tailwindcss/pull/14504))
- _Experimental_: Add CSS codemods for migrating `@layer utilities` and `@layer components` ([#14455](https://github.com/tailwindlabs/tailwindcss/pull/14455))
-- _Experimental_: Add template codemods for migrating important utilities (e.g. `!flex` to `flex!`) ([#14502](https://github.com/tailwindlabs/tailwindcss/pull/14502))
### Fixed
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 6393fbaa8c2b..a4e276e7cda1 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -19,7 +19,7 @@ test(
`,
'src/index.html': html`
🤠👋
-
+
`,
'src/input.css': css`
@tailwind base;
@@ -35,7 +35,7 @@ test(
'src/index.html',
html`
🤠👋
-
+
`,
)
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts
new file mode 100644
index 000000000000..538c1dc6550c
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts
@@ -0,0 +1,59 @@
+import { __unstable__loadDesignSystem } from '@tailwindcss/node'
+import { expect, test } from 'vitest'
+import { automaticVarInjection } from './automatic-var-injection'
+
+test.each([
+ // Arbitrary candidates
+ ['[color:--my-color]', '[color:var(--my-color)]'],
+ ['[--my-color:red]', '[--my-color:red]'],
+ ['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'],
+
+ // Arbitrary values for functional candidates
+ ['bg-[--my-color]', 'bg-[var(--my-color)]'],
+ ['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'],
+ ['border-[length:--my-length]', 'border-[length:var(--my-length)]'],
+ ['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'],
+
+ // Can clean up the workaround for opting out of automatic var injection
+ ['bg-[_--my-color]', 'bg-[--my-color]'],
+ ['bg-[color:_--my-color]', 'bg-[color:--my-color]'],
+ ['border-[length:_--my-length]', 'border-[length:--my-length]'],
+ ['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'],
+
+ // Modifiers
+ ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'],
+ ['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'],
+ ['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'],
+ ['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'],
+
+ // Can clean up the workaround for opting out of automatic var injection
+ ['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'],
+ ['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'],
+ ['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'],
+ ['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'],
+
+ // Variants
+ ['supports-[--test]:flex', 'supports-[var(--test)]:flex'],
+ ['supports-[_--test]:flex', 'supports-[--test]:flex'],
+
+ // Some properties never had var() injection in v3.
+ ['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'],
+ ['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'],
+ ['[view-timeline-name:--myTimeline]', '[view-timeline-name:--myTimeline]'],
+ ['[font-palette:--myPalette]', '[font-palette:--myPalette]'],
+ ['[anchor-name:--myAnchor]', '[anchor-name:--myAnchor]'],
+ ['[anchor-scope:--myScope]', '[anchor-scope:--myScope]'],
+ ['[position-anchor:--myAnchor]', '[position-anchor:--myAnchor]'],
+ ['[position-try-options:--myAnchor]', '[position-try-options:--myAnchor]'],
+ ['[scroll-timeline:--myTimeline]', '[scroll-timeline:--myTimeline]'],
+ ['[animation-timeline:--myAnimation]', '[animation-timeline:--myAnimation]'],
+ ['[view-timeline:--myTimeline]', '[view-timeline:--myTimeline]'],
+ ['[position-try:--myAnchor]', '[position-try:--myAnchor]'],
+])('%s => %s', async (candidate, result) => {
+ let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
+ base: __dirname,
+ })
+
+ let migrated = automaticVarInjection(designSystem, candidate)
+ expect(migrated).toEqual(result)
+})
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts
new file mode 100644
index 000000000000..52512802141d
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.ts
@@ -0,0 +1,155 @@
+import { walk, WalkAction } from '../../../../tailwindcss/src/ast'
+import type { Candidate, Variant } from '../../../../tailwindcss/src/candidate'
+import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
+import { printCandidate } from '../candidates'
+
+export function automaticVarInjection(designSystem: DesignSystem, rawCandidate: string): string {
+ for (let candidate of designSystem.parseCandidate(rawCandidate)) {
+ let didChange = false
+
+ // Add `var(…)` in modifier position, e.g.:
+ //
+ // `bg-red-500/[--my-opacity]` => `bg-red-500/[var(--my-opacity)]`
+ if (
+ 'modifier' in candidate &&
+ candidate.modifier?.kind === 'arbitrary' &&
+ !isAutomaticVarInjectionException(designSystem, candidate, candidate.modifier.value)
+ ) {
+ let { value, didChange: modifierDidChange } = injectVar(candidate.modifier.value)
+ candidate.modifier.value = value
+ didChange ||= modifierDidChange
+ }
+
+ // Add `var(…)` to all variants, e.g.:
+ //
+ // `supports-[--test]:flex'` => `supports-[var(--test)]:flex`
+ for (let variant of candidate.variants) {
+ let didChangeVariant = injectVarIntoVariant(designSystem, variant)
+ if (didChangeVariant) {
+ didChange = true
+ }
+ }
+
+ // Add `var(…)` to arbitrary candidates, e.g.:
+ //
+ // `[color:--my-color]` => `[color:var(--my-color)]`
+ if (
+ candidate.kind === 'arbitrary' &&
+ !isAutomaticVarInjectionException(designSystem, candidate, candidate.value)
+ ) {
+ let { value, didChange: valueDidChange } = injectVar(candidate.value)
+ candidate.value = value
+ didChange ||= valueDidChange
+ }
+
+ // Add `var(…)` to arbitrary values for functional candidates, e.g.:
+ //
+ // `bg-[--my-color]` => `bg-[var(--my-color)]`
+ if (
+ candidate.kind === 'functional' &&
+ candidate.value &&
+ candidate.value.kind === 'arbitrary' &&
+ !isAutomaticVarInjectionException(designSystem, candidate, candidate.value.value)
+ ) {
+ let { value, didChange: valueDidChange } = injectVar(candidate.value.value)
+ candidate.value.value = value
+ didChange ||= valueDidChange
+ }
+
+ if (didChange) {
+ return printCandidate(candidate)
+ }
+ }
+ return rawCandidate
+}
+
+function injectVar(value: string): { value: string; didChange: boolean } {
+ let didChange = false
+ if (value.startsWith('--')) {
+ value = `var(${value})`
+ didChange = true
+ } else if (value.startsWith(' --')) {
+ value = value.slice(1)
+ didChange = true
+ }
+ return { value, didChange }
+}
+
+function injectVarIntoVariant(designSystem: DesignSystem, variant: Variant): boolean {
+ let didChange = false
+ if (
+ variant.kind === 'functional' &&
+ variant.value &&
+ variant.value.kind === 'arbitrary' &&
+ !isAutomaticVarInjectionException(
+ designSystem,
+ createEmptyCandidate(variant),
+ variant.value.value,
+ )
+ ) {
+ let { value, didChange: valueDidChange } = injectVar(variant.value.value)
+ variant.value.value = value
+ didChange ||= valueDidChange
+ }
+
+ if (variant.kind === 'compound') {
+ let compoundDidChange = injectVarIntoVariant(designSystem, variant.variant)
+ if (compoundDidChange) {
+ didChange = true
+ }
+ }
+
+ return didChange
+}
+
+function createEmptyCandidate(variant: Variant) {
+ return {
+ kind: 'arbitrary' as const,
+ property: 'color',
+ value: 'red',
+ modifier: null,
+ variants: [variant],
+ important: false,
+ raw: 'candidate',
+ } satisfies Candidate
+}
+
+const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([
+ // Concrete properties
+ 'scroll-timeline-name',
+ 'timeline-scope',
+ 'view-timeline-name',
+ 'font-palette',
+ 'anchor-name',
+ 'anchor-scope',
+ 'position-anchor',
+ 'position-try-options',
+
+ // Shorthand properties
+ 'scroll-timeline',
+ 'animation-timeline',
+ 'view-timeline',
+ 'position-try',
+])
+// Some properties never had var() injection in v3. We need to convert the candidate to CSS
+// so we can check the properties used by the utility.
+function isAutomaticVarInjectionException(
+ designSystem: DesignSystem,
+ candidate: Candidate,
+ value: string,
+): boolean {
+ let ast = designSystem.compileAstNodes(candidate).map((n) => n.node)
+
+ let isException = false
+ walk(ast, (node) => {
+ if (
+ node.kind === 'declaration' &&
+ AUTO_VAR_INJECTION_EXCEPTIONS.has(node.property) &&
+ node.value == value
+ ) {
+ isException = true
+ return WalkAction.Stop
+ }
+ })
+ return isException
+}
diff --git a/packages/@tailwindcss-upgrade/src/template/migrate.ts b/packages/@tailwindcss-upgrade/src/template/migrate.ts
index 44f2c6215119..c12f59139e1e 100644
--- a/packages/@tailwindcss-upgrade/src/template/migrate.ts
+++ b/packages/@tailwindcss-upgrade/src/template/migrate.ts
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import { extractRawCandidates, replaceCandidateInContent } from './candidates'
+import { automaticVarInjection } from './codemods/automatic-var-injection'
import { bgGradient } from './codemods/bg-gradient'
import { important } from './codemods/important'
@@ -10,7 +11,7 @@ export type Migration = (designSystem: DesignSystem, rawCandidate: string) => st
export default async function migrateContents(
designSystem: DesignSystem,
contents: string,
- migrations: Migration[] = [important, bgGradient],
+ migrations: Migration[] = [important, automaticVarInjection, bgGradient],
): Promise {
let candidates = await extractRawCandidates(contents)