tailwindlabs/tailwindcss · error · Error

`addUtilities({ '${name}' : … })` defines an invalid utility

Error message

`addUtilities({ '${name}' : … })` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. `.scrollbar-none`.

What it means

Thrown by the v4 compatibility layer's `addUtilities` API when every selector passed to it lacks a valid utility class. A valid utility is a single class whose name (after the leading dot) matches `/^[a-z@][a-zA-Z0-9/%._-]*$/` — it must start with a lowercase letter (or @). The walker also intentionally skips classes nested inside `:not(...)`, `:nth-child(... of ...)`, and `:nth-last-child(...)` because those are conditions, not the utility being defined. If no qualifying class is found anywhere in the selector, the call is rejected outright because Tailwind cannot turn it into a registered candidate.

Source

Thrown at packages/tailwindcss/src/compat/plugin-api.ts:326

            node.value = value
            return
          }

          if (
            node.kind === 'function' &&
            (node.value === ':not' ||
              // A class inside `:nth-child(… of <selector>)` is part of the
              // condition, not a utility being defined.
              node.value === ':nth-child' ||
              node.value === ':nth-last-child')
          ) {
            return WalkAction.Skip
          }
        })

        if (!foundValidUtility) {
          throw new Error(
            `\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`,
          )
        }
      }

      for (let [className, ast] of utils) {
        // Prefix all class selector with the configured theme prefix
        if (designSystem.theme.prefix) {
          walk(ast, (node) => {
            if (node.kind === 'rule') {
              let selectorAst = SelectorParser.parse(node.selector)
              walk(selectorAst, (node) => {
                if (node.kind === 'selector' && node.value[0] === '.') {
                  node.value = `.${designSystem.theme.prefix}\\:${node.value.slice(1)}`
                }
              })
              node.selector = SelectorParser.toCss(selectorAst)
            }

View on GitHub (pinned to 16e94cbf7f)

Solutions

  1. Rewrite the selector so it contains exactly one class starting with a lowercase ASCII letter, e.g. `.scrollbar-none`, matching `/^[a-z@][a-zA-Z0-9/%._-]*$/`.
  2. If you need a compound or descendant selector, anchor it on the utility class, e.g. `.my-util > .child` or `.my-util:hover` — the class itself must still be present and valid.
  3. Move purely structural CSS that has no class (element/attribute selectors) into `addBase` instead of `addUtilities`, since `addBase` does not enforce the utility-class rule.
  4. Check the offending plugin name printed in the surrounding stack and upgrade it to a v4-compatible release.

Example fix

// before
plugin(function ({ addUtilities }) {
  addUtilities({
    '.Scrollbar-none': { scrollbarWidth: 'none' },
  })
})
// after
plugin(function ({ addUtilities }) {
  addUtilities({
    '.scrollbar-none': { scrollbarWidth: 'none' },
  })
})
Defensive patterns

Strategy: validation

Validate before calling

// Validate every addUtilities selector before registering
const VALID_CLASS_NAME = /^[a-z@][a-zA-Z0-9/%._-]*$/;
function safeAddUtilities(addUtilities, utilities) {
  for (const selector of Object.keys(utilities)) {
    const ast = parseSelector(selector); // your selector parser
    const hasValid = ast.some(node =>
      node.kind === 'selector' &&
      node.value[0] === '.' &&
      VALID_CLASS_NAME.test(node.value.slice(1))
    );
    if (!hasValid) {
      console.warn(`Skipping invalid utility selector: ${selector}`);
      delete utilities[selector];
    }
  }
  addUtilities(utilities);
}

Type guard

function isValidUtilitySelector(selector: string): boolean {
  // Must contain at least one .class whose name matches the v4 rule
  const matches = selector.match(/\.([a-z@][a-zA-Z0-9/%._-]*)/g);
  return Array.isArray(matches) && matches.length > 0;
}

Try / catch

try {
  addUtilities({ [name]: css });
} catch (e) {
  if (e instanceof Error && e.message.includes('invalid utility selector')) {
    console.warn(`Plugin skipped invalid selector: ${name}`);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: A legacy JS plugin calls `addUtilities({ '.FOO': {...} })` (uppercase first letter), `addUtilities({ 'div': {...} })` (element selector with no class), `addUtilities({ '.a:hover': {...} })` where the only class is consumed inside a pseudo, or `addUtilities({ '.123abc': {...} })` (leading digit). Also triggered by selectors where the sole class lives inside `:not(.x)` so it is skipped by the WalkAction.Skip branch.

Common situations: Porting a v3 plugin to v4 that used complex or compound selectors; third-party plugins that emitted element/attribute selectors; authoring a custom plugin with a typo in the class name (e.g. leading capital or leading digit); a plugin that defined a utility purely as `&[data-x]` with no class.

Related errors


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