Skip to content

perf(css): audit :has() selectors and add stylelint guard for Safari#7708

Open
hectahertz wants to merge 2 commits intomainfrom
hectahertz/perf-css-has-safari-audit
Open

perf(css): audit :has() selectors and add stylelint guard for Safari#7708
hectahertz wants to merge 2 commits intomainfrom
hectahertz/perf-css-has-safari-audit

Conversation

@hectahertz
Copy link
Copy Markdown
Contributor

@hectahertz hectahertz commented Mar 27, 2026

Closes https://github.com/github/github-ui/issues/17224

In Aug 2025, a :has() selector on a broadly-present component caused 10-20+ second freezes on Safari due to quadratic style invalidation in WebKit (398 reactions, 86 participants, 446 points on HN). This PR audits all :has() usage in @primer/react and adds guardrails to prevent a repeat.

Audit summary: 46 :has() occurrences across 12 CSS Module files. All are low-risk because CSS Modules generate unique hashed class names, limiting the invalidation blast radius to a small subtree. The one dangerous pattern (body:has(...) in Dialog) was already mitigated with a feature flag guard.

Changes:

  1. Stylelint rule (selector-pseudo-class-disallowed-list: ['has']) blocks new :has() by default. Developers must add a stylelint-disable with justification.

  2. File-level stylelint-disable comments on all 12 audited files, referencing the tracking issue.

  3. Removed redundant :has() from @primer/styled-react BaseStyles. Same :has([data-color-mode]) pattern already removed from @primer/react in #7325. The global selectors already handle input color-scheme.

  4. :has() guidance in CSS instructions (.github/instructions/css.instructions.md) covering safe vs dangerous patterns.

Full audit with per-selector risk assessment posted on the tracking issue.

Changelog

New

  • Stylelint rule blocking :has() pseudo-class by default with Safari perf warning

Changed

  • Added stylelint-disable audit comments to 12 existing CSS files with :has() usage
  • CSS coding instructions now include :has() guidance

Removed

  • Removed expensive :has([data-color-mode]) selectors from @primer/styled-react BaseStyles (redundant with existing global selectors)

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

  • Verified stylelint catches new :has() usage with clear error message
  • Verified all existing files pass lint with stylelint-disable comments
  • Full npm run lint:css passes clean

Merge checklist

cc @mattcosta7

@hectahertz hectahertz requested a review from a team as a code owner March 27, 2026 17:25
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

⚠️ No Changeset found

Latest commit: f454b9b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Mar 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Audits and hardens :has() selector usage across the Primer React monorepo to prevent Safari/WebKit performance regressions, adding lint guardrails and documenting safe usage patterns.

Changes:

  • Add a Stylelint rule to disallow :has() by default with a Safari performance warning message.
  • Add stylelint-disable audit comments to existing CSS Module files that already rely on :has().
  • Remove redundant :has([data-color-mode]) selectors from @primer/styled-react BaseStyles and document :has() guidance for contributors.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
stylelint.config.mjs Disallows :has() via Stylelint rule with error message guidance.
packages/styled-react/src/components/BaseStyles.tsx Removes :has([data-color-mode]) selectors from global BaseStyles styling.
packages/react/src/experimental/SelectPanel2/SelectPanel.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/TreeView/TreeView.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Timeline/Timeline.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/SegmentedControl/SegmentedControl.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/PageHeader/PageHeader.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Dialog/Dialog.module.css Adds file-level Stylelint disable for audited body:has(...) legacy path.
packages/react/src/ButtonGroup/ButtonGroup.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Button/ButtonBase.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/Breadcrumbs/Breadcrumbs.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/AvatarStack/AvatarStack.module.css Extends existing disables to include the new :has() disallow rule.
packages/react/src/ActionList/Group.module.css Adds file-level Stylelint disable for audited :has() usage.
packages/react/src/ActionList/ActionList.module.css Extends existing disables to include the new :has() disallow rule.
.github/instructions/css.instructions.md Documents :has() risks, safe patterns, and the required lint override approach.

*/
/* stylelint-disable-next-line selector-no-qualifying-type */
/* stylelint-disable-next-line selector-no-qualifying-type, selector-pseudo-class-disallowed-list -- :has() on body guarded by feature flag negation, audited (github/github-ui#17224) */
body:not([data-dialog-scroll-optimized]):has(.Dialog.DisableScroll) {
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.

we can/should just ship the flag here that enables the optimization path soon!

I just keep missing the window's where it's unlocked to do that
#7633

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.

I've added #7633 to my list of “PRs to merge in the next release.” I’ll keep an eye on it and merge it the next time main is unlocked.

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

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants