From e8fa7d0a57fb336e0f44259cb7e64211aaf6d479 Mon Sep 17 00:00:00 2001 From: hectahertz Date: Fri, 27 Mar 2026 18:25:10 +0100 Subject: [PATCH 1/2] perf(css): audit :has() selectors and add stylelint guard for Safari --- .github/instructions/css.instructions.md | 13 +++++++++++++ .../src/ActionList/ActionList.module.css | 2 +- .../react/src/ActionList/Group.module.css | 2 ++ .../src/AvatarStack/AvatarStack.module.css | 2 +- .../src/Breadcrumbs/Breadcrumbs.module.css | 2 ++ .../react/src/Button/ButtonBase.module.css | 2 ++ .../src/ButtonGroup/ButtonGroup.module.css | 2 ++ packages/react/src/Dialog/Dialog.module.css | 2 ++ .../src/PageHeader/PageHeader.module.css | 2 ++ .../SegmentedControl.module.css | 2 ++ .../react/src/Timeline/Timeline.module.css | 2 ++ .../react/src/TreeView/TreeView.module.css | 2 ++ .../SelectPanel2/SelectPanel.module.css | 2 ++ .../src/components/BaseStyles.tsx | 19 ++++++------------- stylelint.config.mjs | 11 +++++++++++ 15 files changed, 52 insertions(+), 15 deletions(-) diff --git a/.github/instructions/css.instructions.md b/.github/instructions/css.instructions.md index f40aee8e99d..f5b59bf2aea 100644 --- a/.github/instructions/css.instructions.md +++ b/.github/instructions/css.instructions.md @@ -11,3 +11,16 @@ If the file ends with `*.module.css`, it is a CSS Module. ## General Conventions - After making a change to a file, use `npx stylelint -q --rd --fix` with a path to the file changed to make sure the code lints + +## `:has()` Selectors and Safari Performance + +`:has()` selectors are blocked by stylelint (`selector-pseudo-class-disallowed-list`) because they can cause catastrophic Safari performance regressions. In Aug 2025, a single `:has()` selector on a broadly-present component froze the entire GitHub UI for 10-20+ seconds on Safari. See [github/github-ui#17224](https://github.com/github/github-ui/issues/17224) for the full audit. + +**When you need `:has()`:** + +1. Ensure it's scoped to a CSS Module class (`&:has(...)` inside a `.Component` rule). Never use `:has()` on `body`, `html`, `*`, or other high-cardinality selectors. +2. Keep the argument simple. Avoid deeply-nested descendant selectors inside `:has()`. +3. Add a file-level stylelint-disable comment with justification: `/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */` +4. Consider alternatives first: data attributes set via JS, `:where()`, or restructuring the DOM so the parent already has the information it needs. + +**Why CSS Modules help:** CSS Module class names are unique hashed identifiers, so the browser only evaluates `:has()` on elements matching that specific class, not the entire DOM. This limits the invalidation blast radius. Unscoped selectors like `body:has(...)` force the browser to scan all descendants of `body` on every DOM mutation, which is where WebKit's quadratic invalidation triggers. diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 29b81e1a630..e378f420a21 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable max-nesting-depth, selector-max-specificity */ +/* stylelint-disable max-nesting-depth, selector-max-specificity, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ .ActionList { padding: 0; diff --git a/packages/react/src/ActionList/Group.module.css b/packages/react/src/ActionList/Group.module.css index 201d3086b86..4ea0b71f2cc 100644 --- a/packages/react/src/ActionList/Group.module.css +++ b/packages/react/src/ActionList/Group.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .Group { list-style: none; diff --git a/packages/react/src/AvatarStack/AvatarStack.module.css b/packages/react/src/AvatarStack/AvatarStack.module.css index b431dec0aab..ba41c5b6206 100644 --- a/packages/react/src/AvatarStack/AvatarStack.module.css +++ b/packages/react/src/AvatarStack/AvatarStack.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable selector-max-specificity */ +/* stylelint-disable selector-max-specificity, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ .AvatarStack { --avatar-border-width: 1px; --mask-size: calc(100% + (var(--avatar-border-width) * 2)); diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index e699fb42587..59004d052bf 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .BreadcrumbsBase { display: flex; justify-content: space-between; diff --git a/packages/react/src/Button/ButtonBase.module.css b/packages/react/src/Button/ButtonBase.module.css index 9d3fee8bc64..58477b99727 100644 --- a/packages/react/src/Button/ButtonBase.module.css +++ b/packages/react/src/Button/ButtonBase.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + /* Base styles */ .ButtonBase { display: flex; diff --git a/packages/react/src/ButtonGroup/ButtonGroup.module.css b/packages/react/src/ButtonGroup/ButtonGroup.module.css index 7c289b69544..a8c163c5b64 100644 --- a/packages/react/src/ButtonGroup/ButtonGroup.module.css +++ b/packages/react/src/ButtonGroup/ButtonGroup.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .ButtonGroup { display: inline-flex; vertical-align: middle; diff --git a/packages/react/src/Dialog/Dialog.module.css b/packages/react/src/Dialog/Dialog.module.css index 0532fed9f22..8cf70ba205e 100644 --- a/packages/react/src/Dialog/Dialog.module.css +++ b/packages/react/src/Dialog/Dialog.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() on body guarded by feature flag negation, audited for Safari perf (github/github-ui#17224) */ + /* The --prc-dialog-scrollgutter property is used only on the body element to * simulate scrollbar-gutter:stable. This property is not and should not * be used elsewhere in the DOM. There is a performance penalty to diff --git a/packages/react/src/PageHeader/PageHeader.module.css b/packages/react/src/PageHeader/PageHeader.module.css index 92486619fe8..3d0a297391c 100644 --- a/packages/react/src/PageHeader/PageHeader.module.css +++ b/packages/react/src/PageHeader/PageHeader.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .PageHeader { /* Grid Row Order */ --grid-row-order-context-area: 1; diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 22b80ae8f33..c3b041b41e3 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .SegmentedControl { /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ --segmented-control-icon-width: 32px; diff --git a/packages/react/src/Timeline/Timeline.module.css b/packages/react/src/Timeline/Timeline.module.css index a841e6b909d..6ae11b52588 100644 --- a/packages/react/src/Timeline/Timeline.module.css +++ b/packages/react/src/Timeline/Timeline.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .Timeline { display: flex; flex-direction: column; diff --git a/packages/react/src/TreeView/TreeView.module.css b/packages/react/src/TreeView/TreeView.module.css index 1ce05985ef6..dcee384793e 100644 --- a/packages/react/src/TreeView/TreeView.module.css +++ b/packages/react/src/TreeView/TreeView.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .TreeViewRootUlStyles { padding: 0; margin: 0; diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css index cebb847f70c..e8a10443cde 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css @@ -1,3 +1,5 @@ +/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ + .Overlay { padding: 0; color: var(--fgColor-default); diff --git a/packages/styled-react/src/components/BaseStyles.tsx b/packages/styled-react/src/components/BaseStyles.tsx index a467e337430..d2ed1412d8d 100644 --- a/packages/styled-react/src/components/BaseStyles.tsx +++ b/packages/styled-react/src/components/BaseStyles.tsx @@ -74,19 +74,12 @@ const GlobalStyle = createGlobalStyle<{colorScheme?: 'light' | 'dark'}>` /* stylelint-disable-next-line primer/colors */ color: var(--BaseStyles-fgColor, var(--fgColor-default)); - /* Global styles for light mode */ - &:has([data-color-mode='light']) { - input & { - color-scheme: light; - } - } - - /* Global styles for dark mode */ - &:has([data-color-mode='dark']) { - input & { - color-scheme: dark; - } - } + /* + * PERFORMANCE: Removed :has([data-color-mode]) selectors that scanned entire DOM. + * Input color-scheme is already handled by global selectors above: + * [data-color-mode='light'] input { color-scheme: light; } + * [data-color-mode='dark'] input { color-scheme: dark; } + */ /* Low-specificity default link styling */ :where(a:not([class*='prc-']):not([class*='PRC-']):not([class*='Primer_Brand__'])) { diff --git a/stylelint.config.mjs b/stylelint.config.mjs index 5eb82300b04..68950a13fd8 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -14,6 +14,17 @@ export default { }, ], 'layer-name-pattern': '^[a-z][a-zA-Z0-9.-]*$', + // :has() can cause severe perf issues in Safari (quadratic style invalidation). + // See github/github-ui#17224 for audit. Existing usages are audited and scoped. + // New usages must be explicitly approved and marked with a stylelint-disable comment. + 'selector-pseudo-class-disallowed-list': [ + ['has'], + { + severity: 'error', + message: + ':has() selectors can cause severe Safari performance issues (github/github-ui#17224). Verify the selector is scoped (CSS Modules) and does not match broadly, then add a stylelint-disable comment.', + }, + ], }, overrides: [ { From f454b9b6a0fac2d2d4af3a01e63b7195d5f876eb Mon Sep 17 00:00:00 2001 From: hectahertz Date: Fri, 27 Mar 2026 18:32:54 +0100 Subject: [PATCH 2/2] refactor(css): use scoped stylelint disables instead of file-level --- .github/instructions/css.instructions.md | 2 +- packages/react/src/ActionList/ActionList.module.css | 10 +++++++++- packages/react/src/ActionList/Group.module.css | 4 +--- packages/react/src/AvatarStack/AvatarStack.module.css | 3 ++- packages/react/src/Breadcrumbs/Breadcrumbs.module.css | 3 +-- packages/react/src/Button/ButtonBase.module.css | 5 +++-- packages/react/src/ButtonGroup/ButtonGroup.module.css | 3 +-- packages/react/src/Dialog/Dialog.module.css | 4 +--- packages/react/src/PageHeader/PageHeader.module.css | 4 ++-- .../src/SegmentedControl/SegmentedControl.module.css | 4 ++-- packages/react/src/Timeline/Timeline.module.css | 3 +-- packages/react/src/TreeView/TreeView.module.css | 3 +-- .../experimental/SelectPanel2/SelectPanel.module.css | 4 +--- stylelint.config.mjs | 2 +- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/instructions/css.instructions.md b/.github/instructions/css.instructions.md index f5b59bf2aea..67f2ffdc75d 100644 --- a/.github/instructions/css.instructions.md +++ b/.github/instructions/css.instructions.md @@ -20,7 +20,7 @@ If the file ends with `*.module.css`, it is a CSS Module. 1. Ensure it's scoped to a CSS Module class (`&:has(...)` inside a `.Component` rule). Never use `:has()` on `body`, `html`, `*`, or other high-cardinality selectors. 2. Keep the argument simple. Avoid deeply-nested descendant selectors inside `:has()`. -3. Add a file-level stylelint-disable comment with justification: `/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */` +3. Add a scoped stylelint disable with justification. Use `stylelint-disable-next-line` for individual selectors, or a `stylelint-disable`/`stylelint-enable` block around a group of related selectors. Avoid file-level disables so new `:has()` selectors in the same file still trigger review. Example: `/* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */` 4. Consider alternatives first: data attributes set via JS, `:where()`, or restructuring the DOM so the parent already has the information it needs. **Why CSS Modules help:** CSS Module class names are unique hashed identifiers, so the browser only evaluates `:has()` on elements matching that specific class, not the entire DOM. This limits the invalidation blast radius. Unscoped selectors like `body:has(...)` force the browser to scan all descendants of `body` on every DOM mutation, which is where WebKit's quadratic invalidation triggers. diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index e378f420a21..de45e397de9 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable max-nesting-depth, selector-max-specificity, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ +/* stylelint-disable max-nesting-depth, selector-max-specificity */ .ActionList { padding: 0; @@ -87,6 +87,7 @@ } /* if a list has a mix of items with and without descriptions, reset the label font-weight to normal */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has([data-has-description='true']):has([data-has-description='false']) { & .ItemLabel { font-weight: var(--base-text-weight-normal); @@ -103,6 +104,7 @@ border-radius: var(--borderRadius-medium); /* apply flex if trailing action exists as an immediate child */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(> .TrailingAction) { display: flex; flex-wrap: nowrap; @@ -371,6 +373,7 @@ } /* When TrailingAction is in loading state, keep labels and descriptions accessible */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.TrailingAction [data-loading='true']):not([data-is-disabled]) { /* Ensure labels and descriptions maintain accessibility contrast */ & .ItemLabel { @@ -546,6 +549,7 @@ } /* show active indicator on parent collapse if child is active */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(~ .SubGroup [data-active='true']) { background: var(--control-transparent-bgColor-selected); @@ -642,6 +646,7 @@ default block */ word-break: normal; } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has([data-truncate='true']) { & .ItemLabel { flex: 1 0 auto; @@ -718,6 +723,7 @@ span wrapping svg or text */ height: 100%; /* Preserve width consistency when loading state is active for text buttons only */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &[data-loading='true']:has([data-component='buttonContent']) { /* Double the left padding to compensate for missing right padding */ padding: 0 0 0 calc(var(--base-size-12) * 2); @@ -737,10 +743,12 @@ span wrapping svg or text */ } .InactiveButtonWrap { + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.TrailingVisual) { grid-area: trailingVisual; } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.LeadingVisual) { grid-area: leadingVisual; } diff --git a/packages/react/src/ActionList/Group.module.css b/packages/react/src/ActionList/Group.module.css index 4ea0b71f2cc..66297a80d78 100644 --- a/packages/react/src/ActionList/Group.module.css +++ b/packages/react/src/ActionList/Group.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .Group { list-style: none; @@ -7,7 +5,7 @@ margin-block-start: var(--base-size-8); /* If somebody tries to pass the `title` prop AND a `NavList.GroupHeading` as a child, hide the `ActionList.GroupHeading */ - /* stylelint-disable-next-line selector-max-specificity */ + /* stylelint-disable-next-line selector-max-specificity, selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.GroupHeadingWrap + ul > .GroupHeadingWrap) { /* stylelint-disable-next-line selector-max-specificity */ & > .GroupHeadingWrap { diff --git a/packages/react/src/AvatarStack/AvatarStack.module.css b/packages/react/src/AvatarStack/AvatarStack.module.css index ba41c5b6206..3eedffd65da 100644 --- a/packages/react/src/AvatarStack/AvatarStack.module.css +++ b/packages/react/src/AvatarStack/AvatarStack.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable selector-max-specificity, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ +/* stylelint-disable selector-max-specificity */ .AvatarStack { --avatar-border-width: 1px; --mask-size: calc(100% + (var(--avatar-border-width) * 2)); @@ -144,6 +144,7 @@ box-shadow: 0 0 0 var(--avatar-border-width) transparent; } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:not([data-component='Avatar']):not(:has([data-square])) { border-radius: 50%; } diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 59004d052bf..bd1691f28f0 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .BreadcrumbsBase { display: flex; justify-content: space-between; @@ -119,6 +117,7 @@ list-style: none; /* allow menu items to wrap line */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.MenuOverlay) { white-space: normal; } diff --git a/packages/react/src/Button/ButtonBase.module.css b/packages/react/src/Button/ButtonBase.module.css index 58477b99727..7d39ed491e9 100644 --- a/packages/react/src/Button/ButtonBase.module.css +++ b/packages/react/src/Button/ButtonBase.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - /* Base styles */ .ButtonBase { display: flex; @@ -26,6 +24,7 @@ justify-content: space-between; gap: var(--base-size-8); + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has([data-kbd-chord]) { padding-inline-end: var(--base-size-6); } @@ -175,6 +174,7 @@ margin-right: var(--control-large-gap); } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has([data-kbd-chord]) { padding-inline-end: var(--base-size-8); } @@ -640,6 +640,7 @@ /* Icon-only + Counter */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:where([data-has-count]):has([data-component='leadingVisual']):not(:has([data-component='text'])) { /* stylelint-disable-next-line primer/spacing */ padding-inline: var(--control-medium-paddingInline-condensed); diff --git a/packages/react/src/ButtonGroup/ButtonGroup.module.css b/packages/react/src/ButtonGroup/ButtonGroup.module.css index a8c163c5b64..cb942aef85c 100644 --- a/packages/react/src/ButtonGroup/ButtonGroup.module.css +++ b/packages/react/src/ButtonGroup/ButtonGroup.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .ButtonGroup { display: inline-flex; vertical-align: middle; @@ -40,6 +38,7 @@ } /* this is a workaround until portal based tooltips are fully removed from dotcom */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(div:last-child:empty) { button, a { diff --git a/packages/react/src/Dialog/Dialog.module.css b/packages/react/src/Dialog/Dialog.module.css index 8cf70ba205e..c5c4e31bf07 100644 --- a/packages/react/src/Dialog/Dialog.module.css +++ b/packages/react/src/Dialog/Dialog.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() on body guarded by feature flag negation, audited for Safari perf (github/github-ui#17224) */ - /* The --prc-dialog-scrollgutter property is used only on the body element to * simulate scrollbar-gutter:stable. This property is not and should not * be used elsewhere in the DOM. There is a performance penalty to @@ -298,7 +296,7 @@ * When the attribute IS present (flag ON), browser skips :has() evaluation * because the :not() check fails first (O(1) attribute lookup). */ -/* 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) { /* stylelint-disable-next-line primer/spacing */ padding-right: var(--prc-dialog-scrollgutter) !important; diff --git a/packages/react/src/PageHeader/PageHeader.module.css b/packages/react/src/PageHeader/PageHeader.module.css index 3d0a297391c..992f1618d12 100644 --- a/packages/react/src/PageHeader/PageHeader.module.css +++ b/packages/react/src/PageHeader/PageHeader.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .PageHeader { /* Grid Row Order */ --grid-row-order-context-area: 1; @@ -36,6 +34,7 @@ --custom-font-size, --custom-line-height, --custom-font-weight are custom properties that can be used to override the below values. We don't want these values to be overridden but still want to allow consumers to override them if needed. */ + /* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited (github/github-ui#17224) */ &:has([data-component='TitleArea'][data-size-variant='large']) { font-size: var(--custom-font-size, var(--text-title-size-large, 2rem)); font-weight: var(--custom-font-weight, var(--base-text-weight-normal, 400)); @@ -165,6 +164,7 @@ padding-block-end: var(--base-size-8); } } + /* stylelint-enable selector-pseudo-class-disallowed-list */ & [data-component='PH_LeadingAction'], & [data-component='PH_TrailingAction'], diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index c3b041b41e3..05bc0feed8f 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .SegmentedControl { /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ --segmented-control-icon-width: 32px; @@ -183,12 +181,14 @@ background-color: var(--borderColor-default); } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(+ [data-selected])::after, &:where([data-selected])::after { background-color: transparent; } } + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:focus-within:has(:focus-visible) { background-color: transparent; } diff --git a/packages/react/src/Timeline/Timeline.module.css b/packages/react/src/Timeline/Timeline.module.css index 6ae11b52588..5f25050c59b 100644 --- a/packages/react/src/Timeline/Timeline.module.css +++ b/packages/react/src/Timeline/Timeline.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .Timeline { display: flex; flex-direction: column; @@ -112,6 +110,7 @@ border: 0; border-top: var(--borderWidth-thicker) solid var(--borderColor-default); + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(+ [data-condensed]) { margin-bottom: calc(-1 * var(--base-size-12)); } diff --git a/packages/react/src/TreeView/TreeView.module.css b/packages/react/src/TreeView/TreeView.module.css index dcee384793e..57e3da65ff8 100644 --- a/packages/react/src/TreeView/TreeView.module.css +++ b/packages/react/src/TreeView/TreeView.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .TreeViewRootUlStyles { padding: 0; margin: 0; @@ -74,6 +72,7 @@ * TreeViewItemContent > TreeViewItemContentText, not a direct child. * This is acceptable as the search is scoped to this element's subtree. */ + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(.TreeViewItemSkeleton):hover { cursor: default; background-color: transparent; diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css index e8a10443cde..16afc9dd10a 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css @@ -1,5 +1,3 @@ -/* stylelint-disable selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited for Safari perf (github/github-ui#17224) */ - .Overlay { padding: 0; color: var(--fgColor-default); @@ -123,7 +121,7 @@ .TextInput { padding-left: var(--base-size-8) !important; - /* stylelint-disable-next-line selector-class-pattern, selector-no-qualifying-type */ + /* stylelint-disable-next-line selector-class-pattern, selector-no-qualifying-type, selector-pseudo-class-disallowed-list -- :has() scoped to CSS Module, audited (github/github-ui#17224) */ &:has(input:placeholder-shown) :global(.TextInput-action) { display: none; } diff --git a/stylelint.config.mjs b/stylelint.config.mjs index 68950a13fd8..6b25674f5f8 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -22,7 +22,7 @@ export default { { severity: 'error', message: - ':has() selectors can cause severe Safari performance issues (github/github-ui#17224). Verify the selector is scoped (CSS Modules) and does not match broadly, then add a stylelint-disable comment.', + ':has() selectors can cause severe Safari performance issues (github/github-ui#17224). Verify the selector is scoped (CSS Modules) and does not match broadly, then add a scoped stylelint disable (e.g. "stylelint-disable-next-line" or a minimal "stylelint-disable"/"stylelint-enable" block), not a file-level disable.', }, ], },