Skip to content

fix(tailwindcss-react-aria-components): make not-* variants compose with native-overlapping states#9974

Merged
snowystinger merged 5 commits into
adobe:mainfrom
mehdibha:fix/not-variant-composition
Jun 9, 2026
Merged

fix(tailwindcss-react-aria-components): make not-* variants compose with native-overlapping states#9974
snowystinger merged 5 commits into
adobe:mainfrom
mehdibha:fix/not-variant-composition

Conversation

@mehdibha

@mehdibha mehdibha commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Problem

not-disabled:, not-invalid:, not-focus:, not-hover: and other not-* utilities built on native-overlapping RAC variants silently emit no CSS.

Tailwind's not-* walker bails when a variant produces > 1 style rule per path (variants.ts#L470-L471), and the dual-selector (array) shape for these variants emits two siblings.

Fix

Collapse both branches into a single :is():where() keeps specificity at (0,1,0), so cascade is unchanged:

- [`&:where([data-rac])${base}`, `&:where(:not([data-rac]))${native}`]
+ `&:is(:where([data-rac])${base}, :where(:not([data-rac]))${native})`

For hover, emit as a CSS-in-JS object (not an @media {...} string) so Tailwind reports compounds as StyleRules | AtRules, keeping group-hover:/peer-hover: composable.

Known limitation

group-not-hover: / peer-not-hover: still don't compose — same architectural constraint that affects native Tailwind's own group-not-hover:.

Test plan

  • not-* on every native-overlapping variant generates CSS
  • group-hover: / peer-hover: / has-hover: unchanged semantics
  • group-not-disabled:, peer-not-disabled:, has-not-disabled:, not-group-hover: work
  • Prefixed mode (rac-*) unchanged

Testing instructions

we can test through the RAC docs once merged. test hover, disabled, press states

…ith native-overlapping states

Tailwind's `not-*` compound walker bails when a variant produces more than
one style rule per path. The dual-selector (array) shape used for variants
that overlap native CSS states (hover, focus, disabled, invalid, etc.)
emitted two sibling rules, so `not-disabled:`, `not-invalid:`, `not-focus:`,
`not-hover:`, and similar utilities silently generated no CSS.

Collapse both branches into a single `:is()` selector. `:where()` keeps
specificity at (0,1,0) so cascade behavior matches the previous output.

Hover additionally needs `@media (hover: hover)` to prevent sticky styles
on touch devices. Emitting it as a CSS-in-JS object (instead of a string
starting with `@media`) causes Tailwind to report the variant's compounds
as `StyleRules | AtRules`, which preserves `group-hover:` / `peer-hover:`
composition while still giving `not-hover:` the "1 style rule + 1 at-rule
per path" shape the walker requires.

Test coverage extended with `not-*`, `group-not-*`, `peer-not-*`,
`has-not-*`, `not-group-*`, `peer-hover:`, and `in-*` variants.
@mehdibha mehdibha marked this pull request as ready for review April 24, 2026 08:16

@yihuiliao yihuiliao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes make sense. I cleaned up some dead code like removing the array branch in mapSelector and wrapSelector since getSelector no longer returns arrays or functions after this change. I ran the tailwind starter locally and spot checked it but would be good for other to verify as well. Once we get this merged, we can test through the RAC docs

mehdibha added a commit to mehdibha/dotUI that referenced this pull request Jun 4, 2026
…s not-* variants) (#172)

* fix: restore input focus ring by patching tailwindcss-react-aria-components

The plugin emits native state variants (invalid, focus, disabled, …) as two
sibling rules — one for RAC elements ([data-rac][data-invalid]) and one for the
native pseudo-class (:not([data-rac]):invalid). Tailwind's not-* compound walker
bails out on multi-rule variants, so every not-invalid: utility silently emitted
no CSS.

The input field relies on focus:not-invalid:border-border-focus and
focus:not-invalid:ring-border-focus-muted, so focused inputs lost their blue
focus border and ring after the move to the official react-aria packages.

Patch getSelector to collapse the two branches into a single :is() selector,
keeping specificity at (0,1,0) via :where(), so not-* negation works again.
Backports adobe/react-spectrum#9974.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(input): apply oxfmt class ordering to styles.ts

Pre-existing formatting drift flagged by oxfmt --check; reordering Tailwind
classes in the source string is behavior-neutral (Tailwind decides precedence
by its own internal sort, not class-attribute order). Keeps CI green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How did you go about verifying the changes you've made here?

.active\\:bg-red {
&:where([data-rac])[data-active] {
.in-hover\\:bg-red {
@media (hover: hover) {

@snowystinger snowystinger Jun 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does Tailwind's processor invert this? I don't think you can have @media inside a class selector. nevermind, i was thinking of old css

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

happy to get this in for testing, thanks

any validation you ran yourself would be much appreciated still

@snowystinger snowystinger added this pull request to the merge queue Jun 9, 2026
Merged via the queue into adobe:main with commit 4612b19 Jun 9, 2026
29 checks passed
@mehdibha

Copy link
Copy Markdown
Contributor Author

Patched it into my RAC-based library (dotUI) via pnpm patch (the exact :is() collapse here) and:

  • diffed generated CSS — not-disabled:/not-invalid:/not-focus:/not-hover: now emit rules, and the non-negated variants (hover:, group-hover:, peer-hover:, has-hover:) stayed byte-identical (:where() keeps specificity at (0,1,0));
  • visually confirmed focus/hover/disabled/press all behave across the library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants