What version of Tailwind CSS are you using?
@tailwindcss/upgrade v4.3.3, migrating a project from v3.4.17 to v4.3.3.
What build tool (or framework if it abstracts the build tool) are you using?
postcss 8.5.26 (the reproduction is a bare PostCSS project; no framework). The bug is in
the upgrade codemod's template migration, so the build tool is incidental.
What version of Node.js are you using?
v25.1.0 (npm 11.6.2)
What browser are you using?
N/A
What operating system are you using?
macOS 26.6.2 (Darwin 25.6.0, arm64)
Reproduction URL
https://github.com/carlos-cubas/tailwind-upgrade-outline-repro
npm install
npx @tailwindcss/upgrade@4.3.3 --force
git diff
Seven files, fourteen lines of TSX. It contains four control cases (the forms #18922
guards, which correctly survive) and four conditional cases plus one comment (which do
not). Verified from a clean clone.
Describe your issue
@tailwindcss/upgrade rewrites variant="outline" to variant="outline-solid" when a
conditional expression sits between variant and the string literal. These are React prop
values, not class names, so the rewrite silently breaks them.
PR #18922 fixed this for the direct forms, and those still work. The guard it added is a
look-behind regex in is-safe-migration.ts:
/variant\s*[:=]\s*\{?['"`]$/
Because it only inspects the text immediately preceding the candidate, any conditional
between variant and the literal defeats it.
From the reproduction — the four control cases are untouched, the four conditional cases
are all rewritten:
|
Input |
After upgrade |
| ✅ |
<Button variant="outline" /> |
unchanged |
| ✅ |
<Button variant={"outline"} /> |
unchanged |
| ✅ |
Button({ variant: "outline" }) |
unchanged |
| ✅ |
function c4({ variant = "outline" }) |
unchanged |
| ❌ |
Button({ variant: isActive ? "outline" : "ghost" }) |
"outline-solid" |
| ❌ |
<Button variant={first ? "default" : "outline"} /> |
"outline-solid" |
| ❌ |
<Button variant={variant ?? "outline"} /> |
"outline-solid" |
| ❌ |
<Button variant={required ? 'secondary' : 'outline'} /> |
"outline-solid" |
Comments are also not excluded from candidate extraction. This line is rewritten too, and
outline-solid: none is not valid CSS:
-// this comment mentions `outline: none` as CSS prose
+// this comment mentions `outline-solid: none` as CSS prose
How loud it is depends on the call site. In our project the codemod hit four call
sites in shadcn/ui-derived components. Three flowed into a cva-typed variant prop and
tsc did reject them:
error TS2322: Type '"default" | "outline-solid"' is not assignable to type
'"link" | "default" | "destructive" | "outline" | "secondary" | "ghost"'.
So for typed variants this is a noisy break rather than a silent one, which is worth
saying plainly. The fourth was a plain data attribute:
<div data-variant={variant ?? "outline"} /> // becomes "outline-solid"
Nothing type-checks that, and the sibling group-data-[variant=ghost]/… selectors that
read it keep working, so the wrong value just sits there. Comment prose is silent for the
same reason. A project on JavaScript, or with a looser variant type, gets no signal on
any of the four.
Expected behaviour. A string literal that is the value of a variant prop should not
be migrated regardless of what expression it sits inside, and candidates inside comments
should not be migrated at all.
I appreciate the immediate look-behind cannot see through an arbitrary expression, so this
may want a different check rather than a longer regex — happy to open a PR against
is-safe-migration.ts with cases added to the existing table in
is-safe-migration.test.ts if you would like it in that form.
What version of Tailwind CSS are you using?
@tailwindcss/upgradev4.3.3, migrating a project from v3.4.17 to v4.3.3.What build tool (or framework if it abstracts the build tool) are you using?
postcss 8.5.26 (the reproduction is a bare PostCSS project; no framework). The bug is in
the upgrade codemod's template migration, so the build tool is incidental.
What version of Node.js are you using?
v25.1.0 (npm 11.6.2)
What browser are you using?
N/A
What operating system are you using?
macOS 26.6.2 (Darwin 25.6.0, arm64)
Reproduction URL
https://github.com/carlos-cubas/tailwind-upgrade-outline-repro
Seven files, fourteen lines of TSX. It contains four control cases (the forms #18922
guards, which correctly survive) and four conditional cases plus one comment (which do
not). Verified from a clean clone.
Describe your issue
@tailwindcss/upgraderewritesvariant="outline"tovariant="outline-solid"when aconditional expression sits between
variantand the string literal. These are React propvalues, not class names, so the rewrite silently breaks them.
PR #18922 fixed this for the direct forms, and those still work. The guard it added is a
look-behind regex in
is-safe-migration.ts:/variant\s*[:=]\s*\{?['"`]$/Because it only inspects the text immediately preceding the candidate, any conditional
between
variantand the literal defeats it.From the reproduction — the four control cases are untouched, the four conditional cases
are all rewritten:
<Button variant="outline" /><Button variant={"outline"} />Button({ variant: "outline" })function c4({ variant = "outline" })Button({ variant: isActive ? "outline" : "ghost" })"outline-solid"<Button variant={first ? "default" : "outline"} />"outline-solid"<Button variant={variant ?? "outline"} />"outline-solid"<Button variant={required ? 'secondary' : 'outline'} />"outline-solid"Comments are also not excluded from candidate extraction. This line is rewritten too, and
outline-solid: noneis not valid CSS:How loud it is depends on the call site. In our project the codemod hit four call
sites in shadcn/ui-derived components. Three flowed into a
cva-typedvariantprop andtscdid reject them:So for typed variants this is a noisy break rather than a silent one, which is worth
saying plainly. The fourth was a plain data attribute:
Nothing type-checks that, and the sibling
group-data-[variant=ghost]/…selectors thatread it keep working, so the wrong value just sits there. Comment prose is silent for the
same reason. A project on JavaScript, or with a looser variant type, gets no signal on
any of the four.
Expected behaviour. A string literal that is the value of a
variantprop should notbe migrated regardless of what expression it sits inside, and candidates inside comments
should not be migrated at all.
I appreciate the immediate look-behind cannot see through an arbitrary expression, so this
may want a different check rather than a longer regex — happy to open a PR against
is-safe-migration.tswith cases added to the existing table inis-safe-migration.test.tsif you would like it in that form.