-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Handle @variant inside @custom-variant
#18885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
828f644 to
6636897
Compare
| name, | ||
| (r) => { | ||
| let body = structuredClone(ast) | ||
| if (usesAtVariant) substituteAtVariant(body, designSystem) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is technically evaluated lazily and every time this function is called instead of ahead of time.
But once we compute a variant we cache it anyway so should be fine.
| { | ||
| compounds: compoundsForSelectors(selectors), | ||
| }, | ||
| { compounds: compoundsForSelectors(selectors) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Custom variants that re-use existing @variants are not compoundable. I had an implementation where the @variant substitution was happening ahead of time to get the selectors out but we have some checks in in-* variant for example that doesn't allow nesting and @variant … is implemented using nesting...
I think this is something we can solve in a separate PR and probably after handling flattening ourselves... can of worms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine imo. The problem here is the nested style rules are the things that disqualify it because otherwise we'll have to implement flattening ourselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, could be a follow-up PR, but this PR on its own is already an improvement.
| 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, () => {}) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep agree
| let seen = new Set<Key>() | ||
| let wip = new Set<Key>() | ||
|
|
||
| let sorted: Key[] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested this with a very simple graph:
let graph = new Map<string, Set<string>>([
['A', new Set(['B', 'C'])],
['B', new Set(['D'])],
['C', new Set(['D'])],
['D', new Set(['E'])],
['E', new Set()],
])But I don't think the real graphs will be much more complex anyway...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'd keep the array in this case. The perf tradeoff makes sense when the memory hit is possibly unbounded but you're likely to hit other perf and memory problems with large CSS files first.
|
Note: we can likely use the topologicalSort utility when handling Might do a follow PR for this. |
| } | ||
|
|
||
| // Update the variant at-rule node, to be the `&` rule node | ||
| replaceWith(node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we never mutate the selector of the & node, then we could skip some layers by using:
| replaceWith(node) | |
| replaceWith(node.nodes) |
But the final result should be the same. This is also the same behavior we had before so I think we can keep it like this.
| // CSS-in-JS object | ||
| else if (typeof variant === 'object') { | ||
| designSystem.variants.fromAst(name, objectToAst(variant)) | ||
| designSystem.variants.fromAst(name, objectToAst(variant), designSystem) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i kinda wish we didn't have to pass the design system into something hanging off the design system :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, one way of solving this is that we put a variantFromAst onto the DesignSystem itself 🤔
or, we can pass in a callback if we want to do something with the ast (body in this case) so the logic lives in the callsite.
I think for now, this is fine because it's all private anyway.
|
Cool. |
This PR contains the following updates: | Package | Change | Age | Confidence | |---|---|---|---| | [@tailwindcss/postcss](https://tailwindcss.com) ([source](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss)) | [`4.1.12` -> `4.1.17`](https://renovatebot.com/diffs/npm/@tailwindcss%2fpostcss/4.1.12/4.1.17) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes <details> <summary>tailwindlabs/tailwindcss (@​tailwindcss/postcss)</summary> ### [`v4.1.17`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4117---2025-11-06) [Compare Source](tailwindlabs/tailwindcss@v4.1.16...v4.1.17) ##### Fixed - Substitute `@variant` inside legacy JS APIs ([#​19263](tailwindlabs/tailwindcss#19263)) - Prevent occasional crash on Windows when loaded into a worker thread ([#​19242](tailwindlabs/tailwindcss#19242)) ### [`v4.1.16`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4116---2025-10-23) [Compare Source](tailwindlabs/tailwindcss@v4.1.15...v4.1.16) ##### Fixed - Discard candidates with an empty data type ([#​19172](tailwindlabs/tailwindcss#19172)) - Fix canonicalization of arbitrary variants with attribute selectors ([#​19176](tailwindlabs/tailwindcss#19176)) - Fix invalid colors due to nested `&` ([#​19184](tailwindlabs/tailwindcss#19184)) - Improve canonicalization for `& > :pseudo` and `& :pseudo` arbitrary variants ([#​19178](tailwindlabs/tailwindcss#19178)) ### [`v4.1.15`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4115---2025-10-20) [Compare Source](tailwindlabs/tailwindcss@v4.1.14...v4.1.15) ##### Fixed - Fix Safari devtools rendering issue due to `color-mix` fallback ([#​19069](tailwindlabs/tailwindcss#19069)) - Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#​19094](tailwindlabs/tailwindcss#19094)) - Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#​19097](tailwindlabs/tailwindcss#19097)) - Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#​19100](tailwindlabs/tailwindcss#19100)) - Prevent important utilities from affecting other utilities ([#​19110](tailwindlabs/tailwindcss#19110)) - Don’t index into strings with the `theme(…)` function ([#​19111](tailwindlabs/tailwindcss#19111)) - Fix parsing issue when `\t` is used in at-rules ([#​19130](tailwindlabs/tailwindcss#19130)) - Upgrade: Canonicalize utilities containing `0` values ([#​19095](tailwindlabs/tailwindcss#19095)) - Upgrade: Migrate deprecated `break-words` to `wrap-break-word` ([#​19157](tailwindlabs/tailwindcss#19157)) ##### Changed - Remove the `postinstall` script from oxide (\[[#​19149](https://github.com/tailwindlabs/tailwindcss/issues/19149)])([#​19149](https://github.com/tailwindlabs/tailwindcss/pull/19149)) ### [`v4.1.14`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4114---2025-10-01) [Compare Source](tailwindlabs/tailwindcss@v4.1.13...v4.1.14) ##### Fixed - Handle `'` syntax in ClojureScript when extracting classes ([#​18888](tailwindlabs/tailwindcss#18888)) - Handle `@variant` inside `@custom-variant` ([#​18885](tailwindlabs/tailwindcss#18885)) - Merge suggestions when using `@utility` ([#​18900](tailwindlabs/tailwindcss#18900)) - Ensure that file system watchers created when using the CLI are always cleaned up ([#​18905](tailwindlabs/tailwindcss#18905)) - Do not generate `grid-column` utilities when configuring `grid-column-start` or `grid-column-end` ([#​18907](tailwindlabs/tailwindcss#18907)) - Do not generate `grid-row` utilities when configuring `grid-row-start` or `grid-row-end` ([#​18907](tailwindlabs/tailwindcss#18907)) - Prevent duplicate CSS when overwriting a static utility with a theme key ([#​18056](tailwindlabs/tailwindcss#18056)) - Show Lightning CSS warnings (if any) when optimizing/minifying ([#​18918](tailwindlabs/tailwindcss#18918)) - Use `default` export condition for `@tailwindcss/vite` ([#​18948](tailwindlabs/tailwindcss#18948)) - Re-throw errors from PostCSS nodes ([#​18373](tailwindlabs/tailwindcss#18373)) - Detect classes in markdown inline directives ([#​18967](tailwindlabs/tailwindcss#18967)) - Ensure files with only `@theme` produce no output when built ([#​18979](tailwindlabs/tailwindcss#18979)) - Support Maud templates when extracting classes ([#​18988](tailwindlabs/tailwindcss#18988)) - Upgrade: Do not migrate `variant = 'outline'` during upgrades ([#​18922](tailwindlabs/tailwindcss#18922)) - Upgrade: Show version mismatch (if any) when running upgrade tool ([#​19028](tailwindlabs/tailwindcss#19028)) - Upgrade: Ensure first class inside `className` is migrated ([#​19031](tailwindlabs/tailwindcss#19031)) - Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#​19031](tailwindlabs/tailwindcss#19031)) ### [`v4.1.13`](https://github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4113---2025-09-03) [Compare Source](tailwindlabs/tailwindcss@v4.1.12...v4.1.13) ##### Changed - Drop warning from browser build ([#​18731](tailwindlabs/tailwindcss#18731)) - Drop exact duplicate declarations when emitting CSS ([#​18809](tailwindlabs/tailwindcss#18809)) ##### Fixed - Don't transition `visibility` when using `transition` ([#​18795](tailwindlabs/tailwindcss#18795)) - Discard matched variants with unknown named values ([#​18799](tailwindlabs/tailwindcss#18799)) - Discard matched variants with non-string values ([#​18799](tailwindlabs/tailwindcss#18799)) - Show suggestions for known `matchVariant` values ([#​18798](tailwindlabs/tailwindcss#18798)) - Replace deprecated `clip` with `clip-path` in `sr-only` ([#​18769](tailwindlabs/tailwindcss#18769)) - Hide internal fields from completions in `matchUtilities` ([#​18820](tailwindlabs/tailwindcss#18820)) - Ignore `.vercel` folders by default (can be overridden by `@source …` rules) ([#​18855](tailwindlabs/tailwindcss#18855)) - Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`) ([#​18869](tailwindlabs/tailwindcss#18869)) - Do not allow custom variants to start or end with a `-` or `_` ([#​18867](tailwindlabs/tailwindcss#18867), [#​18872](tailwindlabs/tailwindcss#18872)) - Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#​18815](tailwindlabs/tailwindcss#18815)) - Upgrade: Migrate `data` theme keys to `@custom-variant` ([#​18816](tailwindlabs/tailwindcss#18816)) - Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#​18817](tailwindlabs/tailwindcss#18817)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNDguNCIsInVwZGF0ZWRJblZlciI6IjQyLjAuMyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> Reviewed-on: https://git.in.csmpro.ru/csmpro/csm-mapban/pulls/28 Co-authored-by: Renovate Bot <renovate@csmpro.ru> Co-committed-by: Renovate Bot <renovate@csmpro.ru>

This PR fixes an issue where you cannot use
@variantinside a@custom-variant. While you can use@variantin normal CSS, you cannot inside of@custom-variant. Today this silently fails and emits invalid CSS.Would result in:
To solve it we have 3 potential solutions:
Some important notes:
The evaluation of the
@custom-variantonly happens when you actually need it. That means that@variantinside@custom-variantwill always have the implementation of the last definition of that variant.In other words, if you use
@variant hoverinside a@custom-variant, and later you override thehovervariant, the@custom-variantwill use the new implementation.If you happen to introduce a circular dependency, then an error will be thrown during the build step.
You can consider it a bug fix or a new feature it's a bit of a gray area. But
one thing that is cool about this is that you can ship a plugin that looks like
this:
And it will use the implementation of
hoverandfocusthat the user has defined. So if they have a customhoverorfocusvariant it will just work.By default
hocus:underlinewould generate:But if you have a custom
hovervariant like:Then
hocus:underlinewould generate:Test plan
Fixes: #18524