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(')) return

View on GitHub (pinned to 16e94cbf7f)

Solutions

  1. 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`).
  2. Strip a leading `@` only if you intentionally include it; otherwise omit it.
  3. 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

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


AI-assisted analysis of tailwindlabs/tailwindcss@16e94cbf7f (2026-08-12). Data as JSON: /api/errors/bd7e813aeb2cf87b. Report an issue: GitHub.