Upgrade codemod rewrites `variant="outline"` to `"outline-solid"` inside conditional expressions
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/[email protected] --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.
Source: tailwindlabs/tailwindcss