Skip to content

perf(css): remove universal * subjects and expand merged :is() selector lists - #4095

Merged
jonrohan merged 3 commits into
mainfrom
copilot/fix-primer-css-selectors
Jun 17, 2026
Merged

perf(css): remove universal * subjects and expand merged :is() selector lists#4095
jonrohan merged 3 commits into
mainfrom
copilot/fix-primer-css-selectors

Conversation

Copilot AI commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Authors: Please fill out this form carefully and completely.

Reviewers: By approving this Pull Request you are approving the code change, as well as its deployment and mitigation plans.
Please read this description carefully. If you feel there is anything unclear or missing, please ask for updates.

What are you trying to accomplish?

On a real github.com trace, several Primer-owned selectors land on the style-recalc slow path (100% non-matching) because their rightmost compound can't be bucketed: a universal * subject, or a merged :is(...) selector list. This cost is paid on every style invalidation, even on pages that ship none of these components.

This addresses the @primer/view-components-owned offenders in the .pcss source:

  • autocomplete-item (top offender, ~10.5 ms) — dropped the … * descendant recolor rule; color is inherited, so setting it on the item recolors descendants without matching every element on the page.

    /* before: forces a match attempt on every element + ancestor walk */
    &.selected, &[aria-selected='true'], &.navigation-focus, &:hover {
      & * { color: inherit !important; }
    }
    
    /* after: children inherit, no `*` rule */
    &.selected, &[aria-selected='true'], &.navigation-focus, &:hover {
      color: var(--fgColor-onEmphasis);
    }
  • Popover-message--* (~6.5 ms) — expanded the nested &::before/&::after caret rules under selector lists into per-class selectors (specificity-neutral; eliminates all merged :is() in the compiled output).

  • breadcrumb-item (~0.9 ms) — expanded the ::after rule into per-class selectors.

  • FormControl-checkbox-wrap/radio-wrap labelWrap — pulled the labelWrap rule out of the nested list so each branch has a single-class (bucketable) subject.

  • Regenerated static/classes.json (expanded Popover selectors surface individual caret-direction classes) and added a patch changeset.

Screenshots

No visual change intended. The autocomplete-item recolor now relies on inheritance rather than an !important universal override — descendants that set their own color are no longer forced to white (the tradeoff called out in the issue).

Integration

No production code updates required; CSS-only refactor. Selector semantics are preserved (Popover/breadcrumb expansions are specificity-neutral; the checkbox-wrapcheckbox-labelWrap pairing is 1:1 in the markup).

List the issues that this change affects.

Risk Assessment

  • Low risk the change is small, highly observable, and easily rolled back.
  • Medium risk changes that are isolated, reduced in scope or could impact few users. The change will not impact library availability.
  • High risk changes are those that could impact customers and SLOs, low or no test coverage, low observability, or slow to rollback.

What approach did you choose and why?

The root cause is bucketing, not :is() itself: a subject compound with no class/tag/id/attr (universal *) or a merged multi-class :is(...) can't be fast-rejected, so Blink tests it against nearly every element on every recalc. Even with postcss noIsPseudoSelector, nesting pseudo-elements/combinators under a selector-list parent still compiles to merged :is(...), so the fix is to author expanded per-class selectors in source.

I scoped this to the safe, high-value offenders (≈18 ms of the measured cost) where expansion is specificity-neutral or behavior-equivalent. ActionListItem--* was intentionally deferred: its :is() branches have differing specificity, so safe expansion needs specificity-preserving restructuring on a core component with visual snapshots — better as a focused follow-up (the "systemic" bucket in the issue).

Anything you want to highlight for special attention from reviewers?

  • The autocomplete-item change is a deliberate behavior shift: removing the * { color: inherit !important } hammer means descendants with explicit colors keep them. Confirm this is acceptable for current autocomplete item content.
  • static/classes.json now lists Popover-message--bottom/--left/--right/--*-top/--*-bottom, previously hidden inside merged :is(); the CSS-selector preview test passes with these surfaced.

Accessibility

  • No new axe scan violation - This change does not introduce any new axe scan violations.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Take a look at the What we look for in reviews section of the contributing guidelines for more information on how we review PRs.

@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cdcdde5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/view-components Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI changed the title [WIP] Fix Primer-owned CSS selectors for better performance perf(css): remove universal * subjects and expand merged :is() selector lists Jun 17, 2026
Copilot AI requested a review from mattcosta7 June 17, 2026 14:33
@mattcosta7
mattcosta7 marked this pull request as ready for review June 17, 2026 14:53
Copilot AI review requested due to automatic review settings June 17, 2026 14:53
@mattcosta7
mattcosta7 requested a review from a team as a code owner June 17, 2026 14:53
@mattcosta7
mattcosta7 requested a review from joshblack June 17, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves CSS style-recalc performance in @primer/view-components by rewriting a handful of selectors that force Blink onto the slow-path (unbucketable universal * subjects and merged :is(...) selector lists produced by nesting pseudo-elements/combinators under selector lists). The changes are scoped to a few high-cost selectors and are designed to preserve selector semantics, with one explicitly called-out behavior shift for autocomplete-item child color overrides.

Changes:

  • Remove the .autocomplete-item &:hover/&.selected … & * { color: inherit !important } universal-descendant recolor rule and rely on inherited color instead.
  • Expand Popover caret (::before/::after) and breadcrumbs ::after rules into explicit per-class selectors to avoid merged :is(...) forms.
  • Refactor FormControl checkbox/radio labelWrap selectors so each branch has a single-class subject; regenerate static/classes.json and add a patch changeset.
Show a summary per file
File Description
app/components/primer/alpha/auto_complete.pcss Drops universal descendant recolor rule; uses inherited color for hover/selected states to avoid slow-path matching.
app/components/primer/beta/popover.pcss Expands caret pseudo-element rules into per-class selectors to prevent merged :is(...) selector lists in compiled output.
app/components/primer/beta/breadcrumbs.pcss Expands ::after suppression rule into explicit selectors to avoid merged :is(...).
app/components/primer/alpha/text_field.pcss Rewrites labelWrap selectors to explicit wrap→labelWrap pairings to keep subjects bucketable.
static/classes.json Surfaces newly explicit Popover caret-direction classes in the class→component mapping.
.changeset/fast-pumas-juggle.md Adds a patch changeset documenting the style-recalc performance improvements.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 0

@jonrohan

Copy link
Copy Markdown
Member

@copilot Fix the stylelint errors

Copilot AI commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Fix the stylelint errors

Fixed in the latest commit. The CI runs stylelint only on changed files, so touching text_field.pcss and popover.pcss surfaced pre-existing errors:

  • text_field.pcss (lines 83, 93, 97, 101, 113): added /* stylelint-disable/enable nesting-selector-no-missing-scoping-root */ around @define-mixin Field — the & selectors inside a mixin have no scoping root by design.
  • popover.pcss (line 20): added /* stylelint-disable-next-line primer/box-shadow */ for the legacy shadow variable fallback that has no direct Primer primitive replacement.

@jonrohan
jonrohan enabled auto-merge (squash) June 17, 2026 20:29
@jonrohan
jonrohan merged commit 6733f3c into main Jun 17, 2026
32 checks passed
@jonrohan
jonrohan deleted the copilot/fix-primer-css-selectors branch June 17, 2026 20:38
@primer primer Bot mentioned this pull request Jun 17, 2026
myabc added a commit to opf/primer_view_components that referenced this pull request Jul 21, 2026
Syncs the fork with primer/view_components up to upstream commit
6733f3c (parent of Release Tracking primer#4098), the last release in
this batch reaching v0.51.6.

Notable upstream changes pulled in:
- classnames.js Set export (primer#4096): static/classnames.{js,cjs,d.ts}
  plus the package.json exports subpath and files entries.
- perf(css): removed universal `*` subjects and expanded merged
  `:is()` selector lists (primer#4095).

Conflicts resolved keeping the fork's identity, scope and its higher
dependency versions; upstream's new exports/files additions and the
static class-name output were merged in and regenerated cleanly.
myabc added a commit to opf/primer_view_components that referenced this pull request Jul 21, 2026
Syncs the fork with primer/view_components up to upstream commit
6733f3c (parent of Release Tracking primer#4098), reaching v0.51.6.

Upstream changes pulled in:
- classnames.js Set export (primer#4096): static/classnames.{js,cjs,d.ts},
  the export-css-selectors collection logic, and the package.json
  exports subpath plus files entries.
- perf(css): removed universal `*` subjects and expanded merged
  `:is()` selector lists (primer#4095).

Conflict resolution kept the fork's identity, scope and its higher
dependency versions; upstream's new exports were merged in. The
recursive tsconfig include is re-applied here so nested component TS
keeps experimentalDecorators under Vite 8/rolldown. All generated
static files were regenerated from a clean tree.
myabc added a commit to opf/primer_view_components that referenced this pull request Jul 21, 2026
Syncs the fork with primer/view_components up to upstream commit
6733f3c (parent of Release Tracking primer#4098), reaching v0.51.6.

Upstream changes pulled in:
- classnames.js Set export (primer#4096): static/classnames.{js,cjs,d.ts},
  the export-css-selectors collection logic, and the package.json
  exports subpath plus files entries.
- perf(css): removed universal `*` subjects and expanded merged
  `:is()` selector lists (primer#4095).

Conflict resolution kept the fork's identity, scope and its higher
dependency versions; upstream's new exports were merged in. The
recursive tsconfig include is re-applied here so nested component TS
keeps experimentalDecorators under Vite 8/rolldown. All generated
static files were regenerated from a clean tree.
myabc added a commit to opf/primer_view_components that referenced this pull request Jul 21, 2026
Syncs the fork with primer/view_components up to upstream commit
6733f3c (parent of Release Tracking primer#4098), reaching v0.51.6.

Upstream changes pulled in:
- classnames.js Set export (primer#4096): static/classnames.{js,cjs,d.ts},
  the export-css-selectors collection logic, and the package.json
  exports subpath plus files entries.
- perf(css): removed universal `*` subjects and expanded merged
  `:is()` selector lists (primer#4095).

Conflict resolution kept the fork's identity, scope and its higher
dependency versions; upstream's new exports were merged in. The
recursive tsconfig include is re-applied here so nested component TS
keeps experimentalDecorators under Vite 8/rolldown. All generated
static files were regenerated from a clean tree.

The upstream css change makes CI lint the whole text_field.pcss, which
surfaced a pre-existing false positive: nesting-selector-no-missing-
scoping-root fires on the & selectors inside the fork's
FormControl-input-width @define-mixin (the & resolves at the @mixin
include site). Silenced with a scoped stylelint-disable.
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.

5 participants