#20435·Tailwind CSS

Upgrade codemod rewrites `variant="outline"` to `"outline-solid"` inside conditional expressions

Author: carlos-cubasCreated Aug 27, 2026Updated Aug 27, 2026

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