Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
register @custom-variant in the correct topological order
  • Loading branch information
RobinMalfait committed Sep 4, 2025
commit 8ef8419611f00a6e2ac1652b8ea3775820bb6a17
42 changes: 36 additions & 6 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { createCssUtility } from './utilities'
import { expand } from './utils/brace-expansion'
import { escape, unescape } from './utils/escape'
import { segment } from './utils/segment'
import { topologicalSort } from './utils/topological-sort'
import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtVariant } from './variants'
export type Config = UserConfig

Expand Down Expand Up @@ -150,7 +151,8 @@ async function parseCss(

let important = null as boolean | null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customVariants = new Map<string, (designSystem: DesignSystem) => void>()
let customVariantDependencies = new Map<string, Set<string>>()
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let firstThemeRule = null as StyleRule | null
let utilitiesNode = null as AtRule | null
Expand Down Expand Up @@ -390,7 +392,7 @@ async function parseCss(
}
}

customVariants.push((designSystem) => {
customVariants.set(name, (designSystem) => {
designSystem.variants.static(
name,
(r) => {
Expand All @@ -411,6 +413,7 @@ async function parseCss(
},
)
})
customVariantDependencies.set(name, new Set<string>())

return
}
Expand All @@ -431,9 +434,17 @@ async function parseCss(
// }
// ```
else {
customVariants.push((designSystem) => {
designSystem.variants.fromAst(name, node.nodes)
let dependencies = new Set<string>()
walk(node.nodes, (child) => {
if (child.kind === 'at-rule' && child.name === '@variant') {
dependencies.add(child.params)
}
})

customVariants.set(name, (designSystem) => {
designSystem.variants.fromAst(name, node.nodes, designSystem)
})
customVariantDependencies.set(name, dependencies)

return
}
Expand Down Expand Up @@ -605,8 +616,27 @@ async function parseCss(
sources,
})

for (let customVariant of customVariants) {
customVariant(designSystem)
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, () => {})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open for suggestions here. Like maybe a designSystem.variants.reserve(name) but felt silly to introduce a new method just for this...

But my idea was to have the same behavior as-if you are overwriting internal variants that should maintain the sort order.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an argument for keeping the position of the lastly defined @custom-variant because otherwise if you were relying on a library that now introduces a @custom-variant with the same name, the sort order will be different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is fine. Keeping the last defined one would probably be more CSS-y but it would make overriding builtin variants different from custom ones and I'd prefer that they act the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agree

}

// 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) {
Expand Down