tailwindlabs/tailwindcss · error · Error
`addVariant('${name}')` defines an invalid variant name. Var
Error message
`addVariant('${name}')` defines an invalid variant name. Variants should only contain alphanumeric, dashes, or underscore characters and start with a lowercase letter or number. What it means
Thrown by the plugin API's `addVariant(name, ...)` when `name` fails `IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(?<![_-])$/`. Variant names must start with a lowercase letter or digit, contain only alphanumeric/underscore/dash, and must not end with `_` or `-`. The optional leading `@` is allowed.
Source
Thrown at packages/tailwindcss/src/compat/plugin-api.ts:122
featuresRef: { current: Features }
referenceMode: boolean
src: SourceLocation | undefined
}): PluginAPI {
let api: PluginAPI = {
addBase(css) {
if (referenceMode) return
let baseNodes = objectToAst(css)
featuresRef.current |= substituteFunctions(baseNodes, designSystem)
let rule = atRule('@layer', 'base', baseNodes)
walk([rule], (node) => {
node.src = src
})
ast.push(rule)
},
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 and start with a lowercase letter or number.`,
)
}
// Ignore variants emitting v3 `:merge(…)` rules. In v4, the `group-*` and `peer-*` variants
// compound automatically.
if (typeof variant === 'string') {
if (variant.includes(':merge(')) return
} else if (Array.isArray(variant)) {
if (variant.some((v) => v.includes(':merge('))) return
} else if (typeof variant === 'object') {
function keyIncludes(object: Record<string, any>, search: string): boolean {
return Object.entries(object).some(
([key, value]) =>
key.includes(search) || (typeof value === 'object' && keyIncludes(value, search)),
)
}
if (keyIncludes(variant, ':merge(')) returnView on GitHub (pinned to 16e94cbf7f)
Solutions
- Rename the variant to match the regex: start `[a-z0-9]`, body `[a-zA-Z0-9_-]*`, no trailing `_` or `-` (e.g. `hover-modal`, `md_2`).
- Strip a leading `@` only if you intentionally include it; otherwise omit it.
- Validate the name with the same regex in your plugin tests before release.
Example fix
// before
plugin(function ({ addVariant }) {
addVariant('Hover-Focus-', '&:hover, &:focus')
})
// after
plugin(function ({ addVariant }) {
addVariant('hover-focus', '&:hover, &:focus')
}) Defensive patterns
Strategy: validation
Validate before calling
const IS_VALID_VARIANT_NAME = /^@?[a-z0-9][a-zA-Z0-9_-]*(?<![_-])$/;
function assertValidVariantName(name) {
if (!IS_VALID_VARIANT_NAME.test(name)) {
throw new Error(`Invalid variant name: ${name}`);
}
}
// assertValidVariantName('hover-focus'); Type guard
function isValidVariantName(name: string): boolean {
return /^@?[a-z0-9][a-zA-Z0-9_-]*(?<![_-])$/.test(name);
} Prevention
- Unit-test `addVariant` names against the regex in plugin test suites.
- Restrict variant naming conventions in a shared style guide for plugin authors.
When it happens
Trigger: Calling `addVariant('Hover', ...)`, `addVariant('2xl-modal', ...)` is fine, but `addVariant('_foo', ...)`, `addVariant('Foo', ...)`, `addVariant('foo!', ...)`, or `addVariant('foo-', ...)` throws. The regex is applied at registration time inside the plugin handler.
Common situations: Authoring a custom plugin with a PascalCase or symbol-containing variant name; copy-pasting a variant name from a selector; trailing dash from string concatenation.
Related errors
- Cannot apply utility class `${candidate}` because the `${unk
- Cannot apply utility class `${candidate}` because the ${unkn
- `@plugin` cannot be nested.
- `@plugin` must have a path.
- Unexpected `@plugin` option: ${toCss([decl])} `@plugin` op
AI-assisted analysis of tailwindlabs/tailwindcss@16e94cbf7f (2026-08-12).
Data as JSON: /api/errors/bd7e813aeb2cf87b.
Report an issue: GitHub.