tailwindlabs/tailwindcss · error · Error

`matchUtilities({ '${name}' : … })` defines an invalid utili

Error message

`matchUtilities({ '${name}' : … })` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter, eg. `scrollbar`.

What it means

Thrown by the compatibility layer's `matchUtilities` API when a key in the `utilities` object fails the `IS_VALID_UTILITY_NAME` regex (`/^[a-z@][a-zA-Z0-9/%._-]*$/`). Unlike `addUtilities` selectors, `matchUtilities` keys are bare names (no leading dot) that become functional utilities like `name-[value]`, so they must be alphanumeric-friendly and start with a lowercase letter (or @). An invalid name cannot be compiled into a candidate, so registration is aborted.

Source

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

        designSystem.utilities.static(className, (candidate) => {
          let clonedAst = ast.map(cloneAstNode)
          replaceNestedClassNameReferences(clonedAst, className, candidate.raw)
          featuresRef.current |= substituteAtApply(clonedAst, designSystem)
          return clonedAst
        })
      }
    },

    matchUtilities(utilities, options) {
      let types = options?.type
        ? Array.isArray(options?.type)
          ? options.type
          : [options.type]
        : ['any']

      for (let [name, fn] of Object.entries(utilities)) {
        if (!IS_VALID_UTILITY_NAME.test(name)) {
          throw new Error(
            `\`matchUtilities({ '${name}' : … })\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter, eg. \`scrollbar\`.`,
          )
        }

        function compileFn({ negative }: { negative: boolean }) {
          return (candidate: Extract<Candidate, { kind: 'functional' }>) => {
            // Throw out any candidate whose value is not a supported type
            if (
              candidate.value?.kind === 'arbitrary' &&
              types.length > 0 &&
              !types.includes('any')
            ) {
              // The candidate has an explicit data type but it's not in the list
              // of supported types by this utility. For example, a `scrollbar`
              // utility that is only used to change the scrollbar color but is
              // used with a `length` value: `scrollbar-[length:var(--whatever)]`
              if (candidate.value.dataType && !types.includes(candidate.value.dataType)) {
                return

View on GitHub (pinned to 16e94cbf7f)

Solutions

  1. Rename the utility key to start with a lowercase letter and use only `[a-zA-Z0-9/%._-]` afterwards, e.g. `scroll`, `col-span`.
  2. Strip any leading dot, space, or underscore from the key — `matchUtilities` wants the bare name, not a selector.
  3. If you need a negative or prefixed variant, rely on the `options.negative` / theme prefix machinery rather than baking symbols into the name.
  4. Validate every key with `/^[a-z@][a-zA-Z0-9/%._-]*$/.test(name)` in your plugin before calling `matchUtilities`.

Example fix

// before
matchUtilities({
  'Scrollbar': (value) => ({ scrollbarColor: value }),
})
// after
matchUtilities({
  'scrollbar': (value) => ({ scrollbarColor: value }),
})
Defensive patterns

Strategy: validation

Validate before calling

const IS_VALID_UTILITY_NAME = /^[a-z@][a-zA-Z0-9/%._-]*$/;
function safeMatchUtilities(matchUtilities, utilities, options) {
  const filtered = {};
  for (const [name, fn] of Object.entries(utilities)) {
    if (IS_VALID_UTILITY_NAME.test(name)) {
      filtered[name] = fn;
    } else {
      console.warn(`Skipping invalid matchUtilities name: ${JSON.stringify(name)}`);
    }
  }
  matchUtilities(filtered, options);
}

Type guard

function isValidUtilityName(name: string): boolean {
  return /^[a-z@][a-zA-Z0-9/%._-]*$/.test(name);
}

Try / catch

try {
  matchUtilities(utilities, options);
} catch (e) {
  if (e instanceof Error && e.message.includes('invalid utility name')) {
    // filter and retry, or warn
  } else throw e;
}

Prevention

When it happens

Trigger: A plugin calls `matchUtilities({ 'Scroll': fn })` (capital first letter), `matchUtilities({ '1col': fn })` (leading digit), `matchUtilities({ 'col width': fn })` (space, not in allowed set), or `matchUtilities({ '_foo': fn })` (leading underscore — underscore is allowed only after the first char). The check runs for every entry via `Object.entries(utilities)`, so one bad key fails the whole call.

Common situations: Migrating a v3 plugin whose utility name began with an underscore or capital; copy-paste from a config object that used display labels; plugin authors assuming `matchUtilities` accepts the same selector syntax as `addUtilities`.

Related errors


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