Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/perf-button-has-selector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@primer/react': patch
---

perf(Button): Replace :has(.Visual) with data-no-visuals attribute

Replace `:has(.Visual)` selectors with `[data-no-visuals]` attribute checks to avoid descendant DOM scans.
Added documentation comments explaining acceptable :has() usage patterns.
19 changes: 13 additions & 6 deletions packages/react/src/Button/ButtonBase.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
justify-content: space-between;
gap: var(--base-size-8);

/* NOTE: Uses descendant :has() - button has very few children (icon, text, kbd). Acceptable. */
&:has([data-kbd-chord]) {
padding-inline-end: var(--base-size-6);
}
Expand Down Expand Up @@ -173,6 +174,7 @@
margin-right: var(--control-large-gap);
}

/* NOTE: Uses descendant :has() - button has very few children (icon, text, kbd). Acceptable. */
&:has([data-kbd-chord]) {
padding-inline-end: var(--base-size-8);
}
Expand Down Expand Up @@ -580,12 +582,13 @@
}
}

/* PERFORMANCE: Use data-no-visuals attribute instead of :has(.Visual) to avoid descendant scan */
[data-a11y-link-underlines='true'] &:where([data-variant='link']) {
&:not(:has(.Visual)) {
&:where([data-no-visuals='true']) {
text-decoration: underline;
}

&:has(.Visual) {
&:not([data-no-visuals='true']) {
background-image: linear-gradient(to right, currentColor, currentColor);
background-size: 100% 1.5px;
background-position: 0 calc(100% - 2px);
Expand All @@ -598,12 +601,12 @@
}

[data-a11y-link-underlines='false'] &:where([data-variant='link']) {
&:not(:has(.Visual)) {
&:where([data-no-visuals='true']) {
text-decoration: none;
background-image: none;
}

&:has(.Visual) {
&:not([data-no-visuals='true']) {
background-image: none;
}
}
Expand Down Expand Up @@ -632,8 +635,12 @@
}
}

/* Icon-only + Counter */

/*
* Icon-only + Counter
* NOTE: Uses descendant :has() - leadingVisual and text are nested inside
* buttonContent wrapper. This is acceptable as the search is scoped to
* this single button element's subtree (small DOM).
*/
&: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);
Expand Down
Loading