tailwindlabs/tailwindcss · error · Error

Circular dependency detected in custom variants: ${output}

Error message

Circular dependency detected in custom variants:

${output}

What it means

Thrown by `topologicalSort` when `@custom-variant` rules reference each other via `@variant` in a way that forms a cycle (e.g. variant A references B, B references C, C references A). The error message renders the cycle as CSS with a `/* ← */` marker on the starting node so the loop is visible. Tailwind refuses to register variants in an undefined order.

Source

Thrown at packages/tailwindcss/src/index.ts:644

  for (let name of customVariants.keys()) {
    // Pre-register the variant to ensure its position in the variant list is
    // based on the order we see them in the CSS.
    designSystem.variants.static(name, () => {})
  }

  // Register custom variants in order
  for (let variant of topologicalSort(customVariantDependencies, {
    onCircularDependency(path, start) {
      let output = toCss(
        path.map((name, idx) => {
          return atRule('@custom-variant', name, [atRule('@variant', path[idx + 1] ?? start, [])])
        }),
      )
        .replaceAll(';', ' { … }')
        .replace(`@custom-variant ${start} {`, `@custom-variant ${start} { /* ← */`)

      throw new Error(`Circular dependency detected in custom variants:\n\n${output}`)
    },
  })) {
    customVariants.get(variant)?.(designSystem)
  }

  for (let customUtility of customUtilities) {
    customUtility(designSystem)
  }

  // Output final set of theme variables at the position of the first
  // `@theme` rule.
  if (firstThemeRule) {
    let nodes = []

    for (let [key, value] of designSystem.theme.entries()) {
      if (value.options & ThemeOptions.REFERENCE) continue
      let node = decl(escape(key), value.value)
      node.src = value.src

View on GitHub (pinned to 16e94cbf7f)

Solutions

  1. Break the cycle: identify the back-edge from the error's rendered output and remove or retarget one `@variant` reference.
  2. If two variants are truly equivalent, alias them in one direction only (`@custom-variant a { @variant b; }` and stop).
  3. Flatten mutually-dependent variants into a single `@custom-variant` that lists all selectors directly.

Example fix

/* before */
@custom-variant a { @variant b; }
@custom-variant b { @variant a; }

/* after */
@custom-variant a (&:hover);
@custom-variant b { @variant a; }
Defensive patterns

Strategy: validation

Validate before calling

// Detect cycles in @custom-variant dependencies before compiling.
import postcss from 'postcss'

function detectCustomVariantCycles(css: string): string[][] {
  const graph = new Map<string, Set<string>>()
  postcss.parse(css).walkAtRules('@custom-variant', (rule) => {
    const name = rule.params.split(' ')[0]
    const deps = new Set<string>()
    rule.walkAtRules('@variant', (v) => deps.add(v.params.split(' ')[0]))
    graph.set(name, deps)
  })

  const cycles: string[][] = []
  const visited = new Set<string>()
  const stack: string[] = []
  function dfs(node: string) {
    if (stack.includes(node)) {
      cycles.push(stack.slice(stack.indexOf(node)).concat(node))
      return
    }
    if (visited.has(node)) return
    visited.add(node)
    stack.push(node)
    for (const dep of graph.get(node) ?? []) dfs(dep)
    stack.pop()
  }
  for (const node of graph.keys()) dfs(node)
  return cycles
}

Prevention

When it happens

Trigger: Defining `@custom-variant a { @variant b; }`, `@custom-variant b { @variant a; }` — or any longer cycle across `@custom-variant` body-form rules.

Common situations: Refactoring variants and accidentally introducing mutual references; renaming a variant but forgetting to update a reference that closes a loop.

Related errors


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