From 54c3f308e912a5b4107cd70625319da4d28ac51f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 3 Sep 2025 16:32:18 +0200 Subject: [PATCH] Do not allow variants to start with `-` (#18867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issue where custom variants with just `-` in the name were allowed but weren't actually picked up by Oxide so you couldn't use them anyway. The reason we allow `-` is for `kebab-style-variants`, which is very common, but you shouldn't use `-`, `--` or more in a variant name. It doesn't really solve the issue (#18863), but it fixes the inconsistencies in that exist today. Inconsistencies: |   | `-:flex` | `--:flex` | | --: | :--: | :--: | | Oxide | ❌ | ❌ | | Tailwind Play | ✅ | ❌ | | Intellisense | ✅ | ✅ | - Oxide already had the correct rules setup, so this is expected - Tailwind Play uses Tailwind's core compile step, but it considers candidates that start with `--` as a CSS variable instead of a utility. This means that the `--:flex` was considered a CSS variable and skipped during compilation. - Intellisense uses the same APIs than Tailwind's core, but it didn't have the CSS variable check which resulted in the `--:flex` being "correct". With this PR, the matrix looks like this now: |   | `-:flex` | `--:flex` | | --: | :--: | :--: | | Oxide | ❌ | ❌ | | Tailwind Play | ❌ | ❌ | | Intellisense | ❌ | ❌ | This should not be considered a breaking change because Oxide didn't pick up candidates with variants that start with a `-`. CSS for these candidates was never generated before. Closes: #18863 --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 1 + packages/tailwindcss/src/compat/plugin-api.ts | 2 +- packages/tailwindcss/src/index.test.ts | 22 ++++++++++++++++++- packages/tailwindcss/src/index.ts | 2 +- packages/tailwindcss/src/variants.ts | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1dcc519..5ece9ec79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hide internal fields from completions in `matchUtilities` ([#18820](https://github.com/tailwindlabs/tailwindcss/pull/18820)) - Ignore `.vercel` folders by default (can be overridden by `@source …` rules) ([#18855](https://github.com/tailwindlabs/tailwindcss/pull/18855)) - Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`) ([#18869](https://github.com/tailwindlabs/tailwindcss/pull/18869)) +- Do not allow custom variants to start with a `-` ([#18867](https://github.com/tailwindlabs/tailwindcss/pull/18867)) - Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815)) - Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816)) - Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817)) diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 42b1c6176..6d13f4678 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -119,7 +119,7 @@ export function buildPluginApi({ addVariant(name, variant) { if (!IS_VALID_VARIANT_NAME.test(name)) { throw new Error( - `\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`, + `\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.`, ) } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index cbdb88186..d66199e6c 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -3768,7 +3768,27 @@ describe('@custom-variant', () => { @custom-variant foo:bar (&:hover, &:focus); `), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: \`@custom-variant foo:bar\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.]`, + `[Error: \`@custom-variant foo:bar\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.]`, + ) + }) + + test('@custom-variant cannot contain dashes on its own', () => { + return expect( + compileCss(css` + @custom-variant - (&.dash); + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: \`@custom-variant -\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.]`, + ) + }) + + test('@custom-variant cannot contain multiple dashes on their own', () => { + return expect( + compileCss(css` + @custom-variant --- (&.dashed); + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: \`@custom-variant ---\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.]`, ) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 07a23a325..4be573efb 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -356,7 +356,7 @@ async function parseCss( if (!IS_VALID_VARIANT_NAME.test(name)) { throw new Error( - `\`@custom-variant ${name}\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`, + `\`@custom-variant ${name}\` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number.`, ) } diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 701f08c0b..c29a936eb 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -18,7 +18,7 @@ import { DefaultMap } from './utils/default-map' import { isPositiveInteger } from './utils/infer-data-type' import { segment } from './utils/segment' -export const IS_VALID_VARIANT_NAME = /^@?[a-zA-Z0-9_-]*$/ +export const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*$/ type VariantFn = ( rule: Rule,