From 3dd34d509a0671150bf4f5dd9ace121f5888b6eb Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Fri, 20 Feb 2026 14:46:29 +0100 Subject: [PATCH 01/10] Add ADR-023: Public data-component API for targeting component parts Establishes data-component as a public, stable API with a consistent naming convention using dot notation mirroring the React component API. This enables consumers to reliably target internal parts of components for style overrides and JS queries despite CSS Module hash changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-data-component-api.md | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 contributor-docs/adrs/adr-023-data-component-api.md diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-data-component-api.md new file mode 100644 index 00000000000..8b7c3039c03 --- /dev/null +++ b/contributor-docs/adrs/adr-023-data-component-api.md @@ -0,0 +1,240 @@ +# Public `data-component` API for targeting component parts + +📆 Date: 2026-02-20 + +## Status + +| Stage | State | +| -------------- | -------------- | +| Status | Proposed ❓ | +| Implementation | | + +## Context + +Primer React uses [CSS Modules](https://github.com/css-modules/css-modules) for +styling. CSS Modules generate hashed class names (e.g., +`prc-ActionList-Item-cBBI`) that are not stable across versions. This makes it +impossible for consumers to reliably target internal parts of a component for +style overrides using class selectors. + +Consumers need the ability to target component parts for legitimate use cases: + +- Customizing the appearance of specific parts (e.g., hiding a trailing visual, + changing the font weight of a label) +- Querying parts of a component in JavaScript (e.g., finding all selected items + in an action list) +- Writing integration tests that target stable selectors + +The `data-component` attribute is already used internally across multiple +components (`Button`, `ActionList`, `PageHeader`, `UnderlineNav`, `Avatar`, and +others) for internal CSS targeting and DOM queries. However, the naming +convention is inconsistent: + +| Pattern | Examples | +| -------------------- | ------------------------------------------------ | +| `ComponentName.Part` | `ActionList.Description`, `ActionList.Selection` | +| `PREFIX_Part` | `PH_LeadingAction`, `PH_Title`, `PH_Navigation` | +| `camelCase` | `buttonContent`, `leadingVisual`, `text` | +| `PascalCase` | `Avatar`, `IconButton`, `SkeletonText` | + +Because `data-component` is not documented as a public API, values have changed +without notice and coverage is incomplete — many component parts have no +`data-component` attribute at all. + +## Decision + +Establish `data-component` as a **public, stable API** for identifying component +parts in the DOM. Every DOM element that represents a component or a meaningful +structural part of a component must include a `data-component` attribute. + +### Naming convention + +Values follow **dot notation mirroring the React component API**, using +PascalCase throughout: + +``` +data-component="ComponentName" → root element +data-component="ComponentName.PartName" → sub-component or internal part +``` + +#### Rules + +1. **Root components** use their React component name in PascalCase. + + ```html + + ``` + +2. **Public sub-components** use dot notation matching the React API. If + consumers write ``, the DOM element gets + `data-component="ActionList.Item"`. + + ```html +
  • + ``` + +3. **Internal structural parts** (DOM elements that are not exposed as a + sub-component but represent a meaningful part of the structure) use the parent + component name followed by a PascalCase part name in dot notation. + + ```html + + + ``` + +4. **State and modifier attributes remain separate.** The `data-component` + attribute identifies _what_ a part is. Existing attributes like + `data-variant`, `data-size`, and `data-loading` describe the _state_ of that + part. These concerns must not be mixed. + + ```html +
  • + ``` + +### Relationship to CSS Modules and CSS Layers + +`data-component` complements the existing styling architecture: + +- **CSS Modules** provide scoped class names for internal styling. Components + continue to use CSS Module classes for their own styles. +- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer + overrides take precedence over component styles regardless of specificity. +- **`data-component`** provides the stable selectors that consumers use to + target parts within those overrides. + +Together, these three mechanisms give consumers a complete override path: + +```css +/* Consumer override — wins over component styles thanks to CSS layers */ +[data-component='ActionList.ItemLabel'] { + font-weight: 600; +} +``` + +### Internal CSS usage + +Components may use `data-component` selectors in their own CSS Modules for +targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` with the +standardized naming: + +```css +/* ButtonBase.module.css */ +& :where([data-component='Button.LeadingVisual']) { + color: var(--button-leadingVisual-fgColor); +} +``` + +### Coverage requirements + +Every component must provide `data-component` on: + +1. The root element +2. Every public sub-component element +3. Every internal structural element that a consumer might reasonably need to + target (labels, content wrappers, visual slots, action slots) + +Elements that are purely for layout and have no semantic meaning (spacers, +wrappers that exist only for CSS grid/flex layout) do not require +`data-component`. + +### Testing requirements + +The presence and value of `data-component` attributes must be covered by tests. +This can be achieved through: + +- Unit tests that assert `data-component` is present on rendered elements +- Snapshot tests that capture the attribute values + +Changing a `data-component` value is a **breaking change** and must follow the +standard breaking change process. + +### Migration + +Existing `data-component` values must be migrated to the new convention. This +migration is a breaking change and should be coordinated as part of a major +release. The following values need to change: + +| Current value | New value | +| --------------------------------------- | --------------------------- | +| `buttonContent` | `Button.Content` | +| `text` (in Button) | `Button.Label` | +| `leadingVisual` (in Button) | `Button.LeadingVisual` | +| `trailingVisual` (in Button) | `Button.TrailingVisual` | +| `trailingAction` (in Button) | `Button.TrailingAction` | +| `ButtonCounter` | `Button.Counter` | +| `PH_LeadingAction` | `PageHeader.LeadingAction` | +| `PH_Breadcrumbs` | `PageHeader.Breadcrumbs` | +| `PH_LeadingVisual` | `PageHeader.LeadingVisual` | +| `PH_Title` | `PageHeader.Title` | +| `PH_TrailingVisual` | `PageHeader.TrailingVisual` | +| `PH_TrailingAction` | `PageHeader.TrailingAction` | +| `PH_Actions` | `PageHeader.Actions` | +| `PH_Navigation` | `PageHeader.Navigation` | +| `TitleArea` | `PageHeader.TitleArea` | +| `GroupHeadingWrap` | `ActionList.GroupHeading` | +| `ActionList.Item--DividerContainer` | `ActionList.ItemSubContent` | +| `icon` (in UnderlineTabbedInterface) | `UnderlineNav.Icon` | +| `text` (in UnderlineTabbedInterface) | `UnderlineNav.Label` | +| `counter` (in UnderlineTabbedInterface) | `UnderlineNav.Counter` | +| `multilineContainer` | `SkeletonText.Container` | +| `input` (in TextInput) | `TextInput.Input` | +| `AnchoredOverlay` (no dot) | `AnchoredOverlay` | +| `ActionBar.VerticalDivider` | `ActionBar.VerticalDivider` | + +Components that currently have no `data-component` on key parts must also be +updated to add them. + +## Consequences + +### Positive + +- **Stable selectors for consumers.** Consumers can target any part of a + component using `[data-component="..."]` selectors that are immune to CSS + Module hash changes and version upgrades. +- **Consistent naming.** A single convention replaces four inconsistent patterns, + making the codebase easier to learn and maintain. +- **Self-documenting.** Inspecting any element in DevTools immediately reveals + what component and part it belongs to — the values map directly to the React + API. +- **Enables JavaScript queries.** Consumers and tests can use + `querySelectorAll('[data-component="ActionList.Item"]')` reliably. +- **Complements CSS Layers.** Together with ADR-021, this gives consumers a + complete, specificity-safe override mechanism. + +### Negative + +- **Breaking change for existing consumers.** Anyone currently relying on the + undocumented `data-component` values (e.g., in CSS overrides or + `querySelector` calls) will need to update when values are renamed. This must + be coordinated in a major release. + +## Alternatives + +### 1. Stable class names alongside CSS Module classes + +Add a non-hashed class name to every part (e.g., +`className={clsx(classes.Item, 'ActionList-item')}`). + +**Why not chosen:** Pollutes the global CSS namespace. Risk of collisions with +consumer or third-party styles. Requires consumers to understand which class +names are "stable" vs. which are CSS Module hashes. Data attributes are a +cleaner separation of concerns — class names for styling, data attributes for +identification. + +### 2. CSS `::part()` pseudo-element + +The `::part()` CSS pseudo-element allows styling of elements inside a shadow +DOM. + +**Why not chosen:** Only works with Shadow DOM, which React does not use. Not +applicable to our architecture. + +### 3. Do nothing — keep `data-component` as an internal implementation detail + +Continue using `data-component` informally without guaranteeing stability. + +**Why not chosen:** Consumers are already depending on these attributes for +overrides (as seen in SelectPanel story CSS). Without a stability guarantee, +any refactor can silently break consumer overrides. Formalizing the API +acknowledges the reality and provides a proper contract. From a5adc101341847f4261a9c50274835275b4cfd24 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 26 Feb 2026 11:30:42 +0100 Subject: [PATCH 02/10] Update ADR-023: split into data-component and data-slot data-component identifies root elements of components and sub-components. data-slot identifies inner structural parts, scoped to their parent component. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-data-component-api.md | 192 ++++++++++-------- 1 file changed, 107 insertions(+), 85 deletions(-) diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-data-component-api.md index 8b7c3039c03..3fb67871fb5 100644 --- a/contributor-docs/adrs/adr-023-data-component-api.md +++ b/contributor-docs/adrs/adr-023-data-component-api.md @@ -1,13 +1,13 @@ -# Public `data-component` API for targeting component parts +# Public `data-component` and `data-slot` API for targeting component parts 📆 Date: 2026-02-20 ## Status -| Stage | State | -| -------------- | -------------- | -| Status | Proposed ❓ | -| Implementation | | +| Stage | State | +| -------------- | ----------- | +| Status | Proposed ❓ | +| Implementation | | ## Context @@ -43,29 +43,31 @@ without notice and coverage is incomplete — many component parts have no ## Decision -Establish `data-component` as a **public, stable API** for identifying component -parts in the DOM. Every DOM element that represents a component or a meaningful -structural part of a component must include a `data-component` attribute. +Establish two **public, stable data attributes** for identifying components and +their parts in the DOM: + +- **`data-component`** — identifies the root element of a component or + sub-component. +- **`data-slot`** — identifies an inner structural part within a component. ### Naming convention -Values follow **dot notation mirroring the React component API**, using -PascalCase throughout: +All values use PascalCase. The two attributes serve distinct roles: ``` -data-component="ComponentName" → root element -data-component="ComponentName.PartName" → sub-component or internal part +data-component="ComponentName" → root element of a component or sub-component +data-slot="PartName" → inner part within a component ``` #### Rules -1. **Root components** use their React component name in PascalCase. +1. **Root components** get `data-component` with their React component name. ```html
      ``` -2. **Public sub-components** use dot notation matching the React API. If +2. **Public sub-components** get `data-component` matching the React API. If consumers write ``, the DOM element gets `data-component="ActionList.Item"`. @@ -73,132 +75,152 @@ data-component="ComponentName.PartName" → sub-component or internal part
    • ``` -3. **Internal structural parts** (DOM elements that are not exposed as a - sub-component but represent a meaningful part of the structure) use the parent - component name followed by a PascalCase part name in dot notation. + Note: a sub-component root uses `data-component`, not `data-slot`, because it + is itself a component — it has its own props, its own identity, and may + contain its own slots. + +3. **Inner structural parts** (DOM elements that are not exposed as a + sub-component but represent a meaningful part of the structure) get + `data-slot` with a PascalCase name describing the part. ```html - - + monalisa + ... + ``` -4. **State and modifier attributes remain separate.** The `data-component` - attribute identifies _what_ a part is. Existing attributes like + Slot names are **scoped to their parent component** — a `Label` slot inside + `ActionList.Item` is distinct from a `Label` slot inside `Button` because + they exist within different `data-component` boundaries. + +4. **State and modifier attributes remain separate.** `data-component` and + `data-slot` identify _what_ an element is. Existing attributes like `data-variant`, `data-size`, and `data-loading` describe the _state_ of that - part. These concerns must not be mixed. + element. These concerns must not be mixed. ```html -
    • +
    • + Delete file +
    • ``` ### Relationship to CSS Modules and CSS Layers -`data-component` complements the existing styling architecture: +`data-component` and `data-slot` complement the existing styling architecture: - **CSS Modules** provide scoped class names for internal styling. Components continue to use CSS Module classes for their own styles. - **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer overrides take precedence over component styles regardless of specificity. -- **`data-component`** provides the stable selectors that consumers use to - target parts within those overrides. +- **`data-component` and `data-slot`** provide the stable selectors that + consumers use to target components and their parts within those overrides. Together, these three mechanisms give consumers a complete override path: ```css -/* Consumer override — wins over component styles thanks to CSS layers */ -[data-component='ActionList.ItemLabel'] { +/* Target a component */ +[data-component='ActionList.Item'] { + border-radius: 8px; +} + +/* Target a slot within a component */ +[data-component='ActionList.Item'] [data-slot='Label'] { font-weight: 600; } ``` ### Internal CSS usage -Components may use `data-component` selectors in their own CSS Modules for -targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` with the -standardized naming: +Components may use `data-slot` selectors in their own CSS Modules for targeting +child parts. This replaces ad-hoc patterns like bare `[data-component='text']` +with the standardized naming: ```css /* ButtonBase.module.css */ -& :where([data-component='Button.LeadingVisual']) { +& :where([data-slot='LeadingVisual']) { color: var(--button-leadingVisual-fgColor); } ``` ### Coverage requirements -Every component must provide `data-component` on: +Every component must provide: -1. The root element -2. Every public sub-component element -3. Every internal structural element that a consumer might reasonably need to - target (labels, content wrappers, visual slots, action slots) +- **`data-component`** on the root element of every component and public + sub-component +- **`data-slot`** on every internal structural element that a consumer might + reasonably need to target (labels, content wrappers, visual slots, action + slots) Elements that are purely for layout and have no semantic meaning (spacers, -wrappers that exist only for CSS grid/flex layout) do not require -`data-component`. +wrappers that exist only for CSS grid/flex layout) do not require either +attribute. ### Testing requirements -The presence and value of `data-component` attributes must be covered by tests. -This can be achieved through: +The presence and values of `data-component` and `data-slot` attributes must be +covered by tests. This can be achieved through: -- Unit tests that assert `data-component` is present on rendered elements +- Unit tests that assert the attributes are present on rendered elements - Snapshot tests that capture the attribute values -Changing a `data-component` value is a **breaking change** and must follow the -standard breaking change process. +Changing a `data-component` or `data-slot` value is a **breaking change** and +must follow the standard breaking change process. ### Migration -Existing `data-component` values must be migrated to the new convention. This -migration is a breaking change and should be coordinated as part of a major -release. The following values need to change: - -| Current value | New value | -| --------------------------------------- | --------------------------- | -| `buttonContent` | `Button.Content` | -| `text` (in Button) | `Button.Label` | -| `leadingVisual` (in Button) | `Button.LeadingVisual` | -| `trailingVisual` (in Button) | `Button.TrailingVisual` | -| `trailingAction` (in Button) | `Button.TrailingAction` | -| `ButtonCounter` | `Button.Counter` | -| `PH_LeadingAction` | `PageHeader.LeadingAction` | -| `PH_Breadcrumbs` | `PageHeader.Breadcrumbs` | -| `PH_LeadingVisual` | `PageHeader.LeadingVisual` | -| `PH_Title` | `PageHeader.Title` | -| `PH_TrailingVisual` | `PageHeader.TrailingVisual` | -| `PH_TrailingAction` | `PageHeader.TrailingAction` | -| `PH_Actions` | `PageHeader.Actions` | -| `PH_Navigation` | `PageHeader.Navigation` | -| `TitleArea` | `PageHeader.TitleArea` | -| `GroupHeadingWrap` | `ActionList.GroupHeading` | -| `ActionList.Item--DividerContainer` | `ActionList.ItemSubContent` | -| `icon` (in UnderlineTabbedInterface) | `UnderlineNav.Icon` | -| `text` (in UnderlineTabbedInterface) | `UnderlineNav.Label` | -| `counter` (in UnderlineTabbedInterface) | `UnderlineNav.Counter` | -| `multilineContainer` | `SkeletonText.Container` | -| `input` (in TextInput) | `TextInput.Input` | -| `AnchoredOverlay` (no dot) | `AnchoredOverlay` | -| `ActionBar.VerticalDivider` | `ActionBar.VerticalDivider` | - -Components that currently have no `data-component` on key parts must also be -updated to add them. +Existing `data-component` values must be migrated to the new convention. Inner +parts move from `data-component` to `data-slot` with simplified names (since +they are scoped to their parent component). This migration is a breaking change +and should be coordinated as part of a major release. + +| Current attr | Current value | New attr | New value | +| ---------------- | --------------------------------------- | ---------------- | --------------------------- | +| `data-component` | `buttonContent` | `data-slot` | `Content` | +| `data-component` | `text` (in Button) | `data-slot` | `Label` | +| `data-component` | `leadingVisual` (in Button) | `data-slot` | `LeadingVisual` | +| `data-component` | `trailingVisual` (in Button) | `data-slot` | `TrailingVisual` | +| `data-component` | `trailingAction` (in Button) | `data-slot` | `TrailingAction` | +| `data-component` | `ButtonCounter` | `data-slot` | `Counter` | +| `data-component` | `PH_LeadingAction` | `data-slot` | `LeadingAction` | +| `data-component` | `PH_Breadcrumbs` | `data-slot` | `Breadcrumbs` | +| `data-component` | `PH_LeadingVisual` | `data-slot` | `LeadingVisual` | +| `data-component` | `PH_Title` | `data-slot` | `Title` | +| `data-component` | `PH_TrailingVisual` | `data-slot` | `TrailingVisual` | +| `data-component` | `PH_TrailingAction` | `data-slot` | `TrailingAction` | +| `data-component` | `PH_Actions` | `data-slot` | `Actions` | +| `data-component` | `PH_Navigation` | `data-slot` | `Navigation` | +| `data-component` | `TitleArea` | `data-slot` | `TitleArea` | +| `data-component` | `GroupHeadingWrap` | `data-component` | `ActionList.GroupHeading` | +| `data-component` | `ActionList.Item--DividerContainer` | `data-slot` | `SubContent` | +| `data-component` | `icon` (in UnderlineTabbedInterface) | `data-slot` | `Icon` | +| `data-component` | `text` (in UnderlineTabbedInterface) | `data-slot` | `Label` | +| `data-component` | `counter` (in UnderlineTabbedInterface) | `data-slot` | `Counter` | +| `data-component` | `multilineContainer` | `data-slot` | `Container` | +| `data-component` | `input` (in TextInput) | `data-slot` | `Input` | +| `data-component` | `AnchoredOverlay` | `data-component` | `AnchoredOverlay` | +| `data-component` | `ActionBar.VerticalDivider` | `data-component` | `ActionBar.VerticalDivider` | + +Components that currently have no attributes on key parts must also be updated. ## Consequences ### Positive -- **Stable selectors for consumers.** Consumers can target any part of a - component using `[data-component="..."]` selectors that are immune to CSS - Module hash changes and version upgrades. +- **Stable selectors for consumers.** Consumers can target any component with + `[data-component="..."]` and any inner part with `[data-slot="..."]` — both + are immune to CSS Module hash changes and version upgrades. +- **Clear separation.** `data-component` answers "which component is this?" + while `data-slot` answers "which part of the component is this?" This makes + the DOM self-documenting and avoids overloading a single attribute. - **Consistent naming.** A single convention replaces four inconsistent patterns, making the codebase easier to learn and maintain. -- **Self-documenting.** Inspecting any element in DevTools immediately reveals - what component and part it belongs to — the values map directly to the React - API. +- **Scoped slot names.** Because `data-slot` values are scoped to their parent + `data-component`, names like `Label` or `LeadingVisual` can be reused across + components without ambiguity. - **Enables JavaScript queries.** Consumers and tests can use - `querySelectorAll('[data-component="ActionList.Item"]')` reliably. + `querySelectorAll('[data-component="ActionList.Item"] [data-slot="Label"]')` + reliably. - **Complements CSS Layers.** Together with ADR-021, this gives consumers a complete, specificity-safe override mechanism. From 49af9d436254a897b151d9edb52a6f2e00766298 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 5 Mar 2026 14:16:16 +0100 Subject: [PATCH 03/10] Rename data-slot to data-part in ADR-023 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-data-component-api.md | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-data-component-api.md index 3fb67871fb5..c470b4d20f0 100644 --- a/contributor-docs/adrs/adr-023-data-component-api.md +++ b/contributor-docs/adrs/adr-023-data-component-api.md @@ -1,4 +1,4 @@ -# Public `data-component` and `data-slot` API for targeting component parts +# Public `data-component` and `data-part` API for targeting component parts 📆 Date: 2026-02-20 @@ -48,7 +48,7 @@ their parts in the DOM: - **`data-component`** — identifies the root element of a component or sub-component. -- **`data-slot`** — identifies an inner structural part within a component. +- **`data-part`** — identifies an inner structural part within a component. ### Naming convention @@ -56,7 +56,7 @@ All values use PascalCase. The two attributes serve distinct roles: ``` data-component="ComponentName" → root element of a component or sub-component -data-slot="PartName" → inner part within a component +data-part="PartName" → inner part within a component ``` #### Rules @@ -75,18 +75,18 @@ data-slot="PartName" → inner part within a component
    • ``` - Note: a sub-component root uses `data-component`, not `data-slot`, because it + Note: a sub-component root uses `data-component`, not `data-part`, because it is itself a component — it has its own props, its own identity, and may contain its own slots. 3. **Inner structural parts** (DOM elements that are not exposed as a sub-component but represent a meaningful part of the structure) get - `data-slot` with a PascalCase name describing the part. + `data-part` with a PascalCase name describing the part. ```html - monalisa - ... - + monalisa + ... + ``` Slot names are **scoped to their parent component** — a `Label` slot inside @@ -94,25 +94,25 @@ data-slot="PartName" → inner part within a component they exist within different `data-component` boundaries. 4. **State and modifier attributes remain separate.** `data-component` and - `data-slot` identify _what_ an element is. Existing attributes like + `data-part` identify _what_ an element is. Existing attributes like `data-variant`, `data-size`, and `data-loading` describe the _state_ of that element. These concerns must not be mixed. ```html
    • - Delete file + Delete file
    • ``` ### Relationship to CSS Modules and CSS Layers -`data-component` and `data-slot` complement the existing styling architecture: +`data-component` and `data-part` complement the existing styling architecture: - **CSS Modules** provide scoped class names for internal styling. Components continue to use CSS Module classes for their own styles. - **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer overrides take precedence over component styles regardless of specificity. -- **`data-component` and `data-slot`** provide the stable selectors that +- **`data-component` and `data-part`** provide the stable selectors that consumers use to target components and their parts within those overrides. Together, these three mechanisms give consumers a complete override path: @@ -124,20 +124,20 @@ Together, these three mechanisms give consumers a complete override path: } /* Target a slot within a component */ -[data-component='ActionList.Item'] [data-slot='Label'] { +[data-component='ActionList.Item'] [data-part='Label'] { font-weight: 600; } ``` ### Internal CSS usage -Components may use `data-slot` selectors in their own CSS Modules for targeting +Components may use `data-part` selectors in their own CSS Modules for targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` with the standardized naming: ```css /* ButtonBase.module.css */ -& :where([data-slot='LeadingVisual']) { +& :where([data-part='LeadingVisual']) { color: var(--button-leadingVisual-fgColor); } ``` @@ -148,7 +148,7 @@ Every component must provide: - **`data-component`** on the root element of every component and public sub-component -- **`data-slot`** on every internal structural element that a consumer might +- **`data-part`** on every internal structural element that a consumer might reasonably need to target (labels, content wrappers, visual slots, action slots) @@ -158,46 +158,46 @@ attribute. ### Testing requirements -The presence and values of `data-component` and `data-slot` attributes must be +The presence and values of `data-component` and `data-part` attributes must be covered by tests. This can be achieved through: - Unit tests that assert the attributes are present on rendered elements - Snapshot tests that capture the attribute values -Changing a `data-component` or `data-slot` value is a **breaking change** and +Changing a `data-component` or `data-part` value is a **breaking change** and must follow the standard breaking change process. ### Migration Existing `data-component` values must be migrated to the new convention. Inner -parts move from `data-component` to `data-slot` with simplified names (since +parts move from `data-component` to `data-part` with simplified names (since they are scoped to their parent component). This migration is a breaking change and should be coordinated as part of a major release. | Current attr | Current value | New attr | New value | | ---------------- | --------------------------------------- | ---------------- | --------------------------- | -| `data-component` | `buttonContent` | `data-slot` | `Content` | -| `data-component` | `text` (in Button) | `data-slot` | `Label` | -| `data-component` | `leadingVisual` (in Button) | `data-slot` | `LeadingVisual` | -| `data-component` | `trailingVisual` (in Button) | `data-slot` | `TrailingVisual` | -| `data-component` | `trailingAction` (in Button) | `data-slot` | `TrailingAction` | -| `data-component` | `ButtonCounter` | `data-slot` | `Counter` | -| `data-component` | `PH_LeadingAction` | `data-slot` | `LeadingAction` | -| `data-component` | `PH_Breadcrumbs` | `data-slot` | `Breadcrumbs` | -| `data-component` | `PH_LeadingVisual` | `data-slot` | `LeadingVisual` | -| `data-component` | `PH_Title` | `data-slot` | `Title` | -| `data-component` | `PH_TrailingVisual` | `data-slot` | `TrailingVisual` | -| `data-component` | `PH_TrailingAction` | `data-slot` | `TrailingAction` | -| `data-component` | `PH_Actions` | `data-slot` | `Actions` | -| `data-component` | `PH_Navigation` | `data-slot` | `Navigation` | -| `data-component` | `TitleArea` | `data-slot` | `TitleArea` | +| `data-component` | `buttonContent` | `data-part` | `Content` | +| `data-component` | `text` (in Button) | `data-part` | `Label` | +| `data-component` | `leadingVisual` (in Button) | `data-part` | `LeadingVisual` | +| `data-component` | `trailingVisual` (in Button) | `data-part` | `TrailingVisual` | +| `data-component` | `trailingAction` (in Button) | `data-part` | `TrailingAction` | +| `data-component` | `ButtonCounter` | `data-part` | `Counter` | +| `data-component` | `PH_LeadingAction` | `data-part` | `LeadingAction` | +| `data-component` | `PH_Breadcrumbs` | `data-part` | `Breadcrumbs` | +| `data-component` | `PH_LeadingVisual` | `data-part` | `LeadingVisual` | +| `data-component` | `PH_Title` | `data-part` | `Title` | +| `data-component` | `PH_TrailingVisual` | `data-part` | `TrailingVisual` | +| `data-component` | `PH_TrailingAction` | `data-part` | `TrailingAction` | +| `data-component` | `PH_Actions` | `data-part` | `Actions` | +| `data-component` | `PH_Navigation` | `data-part` | `Navigation` | +| `data-component` | `TitleArea` | `data-part` | `TitleArea` | | `data-component` | `GroupHeadingWrap` | `data-component` | `ActionList.GroupHeading` | -| `data-component` | `ActionList.Item--DividerContainer` | `data-slot` | `SubContent` | -| `data-component` | `icon` (in UnderlineTabbedInterface) | `data-slot` | `Icon` | -| `data-component` | `text` (in UnderlineTabbedInterface) | `data-slot` | `Label` | -| `data-component` | `counter` (in UnderlineTabbedInterface) | `data-slot` | `Counter` | -| `data-component` | `multilineContainer` | `data-slot` | `Container` | -| `data-component` | `input` (in TextInput) | `data-slot` | `Input` | +| `data-component` | `ActionList.Item--DividerContainer` | `data-part` | `SubContent` | +| `data-component` | `icon` (in UnderlineTabbedInterface) | `data-part` | `Icon` | +| `data-component` | `text` (in UnderlineTabbedInterface) | `data-part` | `Label` | +| `data-component` | `counter` (in UnderlineTabbedInterface) | `data-part` | `Counter` | +| `data-component` | `multilineContainer` | `data-part` | `Container` | +| `data-component` | `input` (in TextInput) | `data-part` | `Input` | | `data-component` | `AnchoredOverlay` | `data-component` | `AnchoredOverlay` | | `data-component` | `ActionBar.VerticalDivider` | `data-component` | `ActionBar.VerticalDivider` | @@ -208,18 +208,18 @@ Components that currently have no attributes on key parts must also be updated. ### Positive - **Stable selectors for consumers.** Consumers can target any component with - `[data-component="..."]` and any inner part with `[data-slot="..."]` — both + `[data-component="..."]` and any inner part with `[data-part="..."]` — both are immune to CSS Module hash changes and version upgrades. - **Clear separation.** `data-component` answers "which component is this?" - while `data-slot` answers "which part of the component is this?" This makes + while `data-part` answers "which part of the component is this?" This makes the DOM self-documenting and avoids overloading a single attribute. - **Consistent naming.** A single convention replaces four inconsistent patterns, making the codebase easier to learn and maintain. -- **Scoped slot names.** Because `data-slot` values are scoped to their parent +- **Scoped slot names.** Because `data-part` values are scoped to their parent `data-component`, names like `Label` or `LeadingVisual` can be reused across components without ambiguity. - **Enables JavaScript queries.** Consumers and tests can use - `querySelectorAll('[data-component="ActionList.Item"] [data-slot="Label"]')` + `querySelectorAll('[data-component="ActionList.Item"] [data-part="Label"]')` reliably. - **Complements CSS Layers.** Together with ADR-021, this gives consumers a complete, specificity-safe override mechanism. From 1883137a85d9d1d7694d0820acd68dfa89263362 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 5 Mar 2026 14:30:36 +0100 Subject: [PATCH 04/10] Add versioning and breaking changes section to ADR-023 Add a dedicated subsection to ADR-023 documenting what constitutes a breaking vs non-breaking change for data-component and data-part attributes (semver classification). Also adds corresponding rows to the Changes table in contributor-docs/versioning.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-data-component-api.md | 23 +++++++++++++++++++ contributor-docs/versioning.md | 2 ++ 2 files changed, 25 insertions(+) diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-data-component-api.md index c470b4d20f0..01bf29f0dae 100644 --- a/contributor-docs/adrs/adr-023-data-component-api.md +++ b/contributor-docs/adrs/adr-023-data-component-api.md @@ -167,6 +167,29 @@ covered by tests. This can be achieved through: Changing a `data-component` or `data-part` value is a **breaking change** and must follow the standard breaking change process. +### Versioning and breaking changes + +Because `data-component` and `data-part` are **public API**, changes to them +follow [Semantic Versioning](../versioning.md). The table below summarises what +requires which kind of release: + +| Change | semver bump | +| ------------------------------------------------------------------ | ----------- | +| A `data-component` or `data-part` attribute is added to an element | `minor` | +| A `data-component` or `data-part` value is renamed | `major` | +| A `data-component` or `data-part` attribute is removed | `major` | +| An attribute is moved from one element to another | `major` | +| A `data-component` is changed to `data-part` or vice-versa | `major` | + +**Deprecation path.** Before removing or renaming a value in a major release, +the old value should be deprecated in at least one prior minor release. During +the deprecation window the component must emit a development-mode console +warning (using the existing `warn` / `deprecate` helpers) so consumers have +time to migrate. + +The [Migration](#migration) table below captures the full set of renames +planned for the next major release. + ### Migration Existing `data-component` values must be migrated to the new convention. Inner diff --git a/contributor-docs/versioning.md b/contributor-docs/versioning.md index 733e8538be9..ab12a4312a0 100644 --- a/contributor-docs/versioning.md +++ b/contributor-docs/versioning.md @@ -68,6 +68,8 @@ For a full list of releases, visit our [releases](https://github.com/primer/reac | | [A component changes its usage of a CSS Custom Property](#a-component-changes-its-usage-of-a-css-custom-property) | potentially `major` | | Accessibility | [A component includes a landmark role](#a-component-includes-a-landmark-role) | potentially `major` | | | [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) | potentially `major` | +| Data attrs | A `data-component` or `data-part` attribute is added ([ADR-023](./adrs/adr-023-data-component-api.md)) | `minor` | +| | A `data-component` or `data-part` value is renamed, removed, or moved to a different element | `major` | ## Reference From 5e07cb17619a3f7adf48e7fc97f8e0bd987c92a0 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 5 Mar 2026 14:45:13 +0100 Subject: [PATCH 05/10] Add versioning section --- .../adrs/adr-023-data-component-api.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-data-component-api.md index 01bf29f0dae..867856694ec 100644 --- a/contributor-docs/adrs/adr-023-data-component-api.md +++ b/contributor-docs/adrs/adr-023-data-component-api.md @@ -190,6 +190,29 @@ time to migrate. The [Migration](#migration) table below captures the full set of renames planned for the next major release. +### Versioning and breaking changes + +Because `data-component` and `data-part` are **public API**, changes to them +follow [Semantic Versioning](../versioning.md). The table below summarises what +requires which kind of release: + +| Change | semver bump | +| ------------------------------------------------------------------ | ----------- | +| A `data-component` or `data-part` attribute is added to an element | `minor` | +| A `data-component` or `data-part` value is renamed | `major` | +| A `data-component` or `data-part` attribute is removed | `major` | +| An attribute is moved from one element to another | `major` | +| A `data-component` is changed to `data-part` or vice-versa | `major` | + +**Deprecation path.** Before removing or renaming a value in a major release, +the old value should be deprecated in at least one prior minor release. During +the deprecation window the component must emit a development-mode console +warning (using the existing `warn` / `deprecate` helpers) so consumers have +time to migrate. + +The [Migration](#migration) table below captures the full set of renames +planned for the next major release. + ### Migration Existing `data-component` values must be migrated to the new convention. Inner From 02ddb5c987d3cac2c9a3944f58d59408f144c0e3 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 5 Mar 2026 14:48:01 +0100 Subject: [PATCH 06/10] add data-attr changes to versioning table --- contributor-docs/versioning.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/contributor-docs/versioning.md b/contributor-docs/versioning.md index ab12a4312a0..88b4621c69b 100644 --- a/contributor-docs/versioning.md +++ b/contributor-docs/versioning.md @@ -7,17 +7,19 @@ ## Table of Contents -- [Overview](#overview) -- [Changes](#changes) -- [Reference](#reference) - - [The type of a prop is broadened](#the-type-of-a-prop-is-broadened) - - [The type of a prop is narrowed](#the-type-of-a-prop-is-narrowed) - - [The `display` property used for the container of `children` is changed](#the-display-property-used-for-the-container-of-children-is-changed) - - [A component changes its usage of a CSS Custom Property](#a-component-changes-its-usage-of-a-css-custom-property) - - [A component includes a landmark role](#a-component-includes-a-landmark-role) - - [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) - - [The element onto which props are spread is changed](#the-element-onto-which-props-are-spread-is-changed) - - [The element type in an event handler becomes broader](#the-element-type-in-an-event-handler-becomes-broader) +- [Versioning](#versioning) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Changes](#changes) + - [Reference](#reference) + - [The type of a prop is broadened](#the-type-of-a-prop-is-broadened) + - [The type of a prop is narrowed](#the-type-of-a-prop-is-narrowed) + - [The `display` property used for the container of `children` is changed](#the-display-property-used-for-the-container-of-children-is-changed) + - [A component changes its usage of a CSS Custom Property](#a-component-changes-its-usage-of-a-css-custom-property) + - [A component includes a landmark role](#a-component-includes-a-landmark-role) + - [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) + - [The element onto which props are spread is changed](#the-element-onto-which-props-are-spread-is-changed) + - [The element type in an event handler becomes broader](#the-element-type-in-an-event-handler-becomes-broader) @@ -70,6 +72,8 @@ For a full list of releases, visit our [releases](https://github.com/primer/reac | | [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) | potentially `major` | | Data attrs | A `data-component` or `data-part` attribute is added ([ADR-023](./adrs/adr-023-data-component-api.md)) | `minor` | | | A `data-component` or `data-part` value is renamed, removed, or moved to a different element | `major` | +| Data attrs | A `data-component` or `data-part` attribute is added ([ADR-023](./adrs/adr-023-data-component-api.md)) | `minor` | +| | A `data-component` or `data-part` value is renamed, removed, or moved to a different element | `major` | ## Reference From 176b2b577c29da55e4c801780712ccd4cc45b37e Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Thu, 19 Mar 2026 01:00:38 -0400 Subject: [PATCH 07/10] update stable selectors api --- ...api.md => adr-023-stable-selectors-api.md} | 272 +++++++++++------- 1 file changed, 169 insertions(+), 103 deletions(-) rename contributor-docs/adrs/{adr-023-data-component-api.md => adr-023-stable-selectors-api.md} (56%) diff --git a/contributor-docs/adrs/adr-023-data-component-api.md b/contributor-docs/adrs/adr-023-stable-selectors-api.md similarity index 56% rename from contributor-docs/adrs/adr-023-data-component-api.md rename to contributor-docs/adrs/adr-023-stable-selectors-api.md index 867856694ec..998b5d11de4 100644 --- a/contributor-docs/adrs/adr-023-data-component-api.md +++ b/contributor-docs/adrs/adr-023-stable-selectors-api.md @@ -1,4 +1,4 @@ -# Public `data-component` and `data-part` API for targeting component parts +# Stable identifiers for Primer components 📆 Date: 2026-02-20 @@ -7,23 +7,29 @@ | Stage | State | | -------------- | ----------- | | Status | Proposed ❓ | -| Implementation | | +| Implementation | Pending ⚠️ | ## Context -Primer React uses [CSS Modules](https://github.com/css-modules/css-modules) for -styling. CSS Modules generate hashed class names (e.g., -`prc-ActionList-Item-cBBI`) that are not stable across versions. This makes it -impossible for consumers to reliably target internal parts of a component for -style overrides using class selectors. - -Consumers need the ability to target component parts for legitimate use cases: - -- Customizing the appearance of specific parts (e.g., hiding a trailing visual, - changing the font weight of a label) -- Querying parts of a component in JavaScript (e.g., finding all selected items - in an action list) -- Writing integration tests that target stable selectors +Primer React components do not expose stable identifiers for their rendered DOM +elements. Consumers who need to reference a specific component or slot in the +DOM — for testing, tracking, monitoring, or querying — have no reliable way to +do so. + +This creates problems across several areas: + +- **Testing.** Unit and end-to-end tests resort to brittle selectors like CSS + Module hashes (`prc-ActionList-Item-cBBI`), DOM structure assumptions, or + text content matching — all of which break silently when Primer is updated. +- **Tracking and monitoring.** Consumers that want to measure how components are + used in production (e.g., counting how many ActionList items appear on a page) + have no stable hook to query against. +- **JavaScript queries.** Finding all instances of a component or its parts in + the DOM (e.g., all selected items in an action list) requires knowledge of + internal implementation details. +- **Style overrides.** When consumers do need to customize appearance, CSS + Module hashes are not stable across versions, leaving no reliable selector to + target. The `data-component` attribute is already used internally across multiple components (`Button`, `ActionList`, `PageHeader`, `UnderlineNav`, `Avatar`, and @@ -43,14 +49,31 @@ without notice and coverage is incomplete — many component parts have no ## Decision -Establish two **public, stable data attributes** for identifying components and -their parts in the DOM: +Establish two **public, stable data attributes** that act as reliable +identifiers for Primer components and their parts in the DOM. Think of these as +built-in test IDs — stable, predictable anchors that consumers can use for +testing, tracking, monitoring, and querying without coupling to internal DOM +structure. - **`data-component`** — identifies the root element of a component or sub-component. - **`data-part`** — identifies an inner structural part within a component. -### Naming convention +These attributes are primarily intended for: + +1. **Testing** — use them as locators in unit tests, integration tests, and + end-to-end tests instead of brittle class names or DOM paths. +2. **Tracking and monitoring** — query the DOM for component usage metrics, + analytics, or observability. +3. **JavaScript queries** — find specific components or parts + programmatically (e.g., + `document.querySelectorAll('[data-component="ActionList.Item"]')`). +4. **Simple CSS overrides** — as an added benefit, these selectors also + provide a stable target for style customizations when needed. + +### Implementation details + +#### Naming convention All values use PascalCase. The two attributes serve distinct roles: @@ -59,7 +82,7 @@ data-component="ComponentName" → root element of a component or sub-compon data-part="PartName" → inner part within a component ``` -#### Rules +##### Rules 1. **Root components** get `data-component` with their React component name. @@ -104,32 +127,46 @@ data-part="PartName" → inner part within a component ``` -### Relationship to CSS Modules and CSS Layers +#### `data-component` and `data-part` on all components and slots -`data-component` and `data-part` complement the existing styling architecture: +All Primer components will receive a `data-component` attribute on their root +element, and all meaningful slots will receive a `data-part` attribute. These +attributes serve as the stable identifiers that consumers can use for targeting, +ensuring full coverage across the library. -- **CSS Modules** provide scoped class names for internal styling. Components - continue to use CSS Module classes for their own styles. -- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer - overrides take precedence over component styles regardless of specificity. -- **`data-component` and `data-part`** provide the stable selectors that - consumers use to target components and their parts within those overrides. +Every component must provide: + +- **`data-component`** on the root element of every component and public + sub-component +- **`data-part`** on every internal structural element that a consumer might + reasonably need to target (labels, content wrappers, visual slots, action + slots) -Together, these three mechanisms give consumers a complete override path: +Elements that are purely for layout and have no semantic meaning (spacers, +wrappers that exist only for CSS grid/flex layout) do not require either +attribute. -```css -/* Target a component */ -[data-component='ActionList.Item'] { - border-radius: 8px; -} +#### ESLint rule in `eslint-plugin-primer-react` -/* Target a slot within a component */ -[data-component='ActionList.Item'] [data-part='Label'] { - font-weight: 600; -} +A new ESLint rule will be added to `eslint-plugin-primer-react` that **warns** +whenever a `data-component` or `data-part` selector is used in consumer code. The warning will +inform consumers of the implications of relying on these selectors — namely that +the surrounding DOM structure, attributes, parents, and children of the targeted +element are not guaranteed to be stable. + +Consumers must **explicitly override** the +rule on a per-usage basis to use a `data-component` selector. Overriding the +rule acts as an acknowledgment that the consumer understands the risks and +accepts responsibility for any breakage caused by structural changes to the +component's DOM. + +```js +/* eslint-disable primer-react/no-data-component-selector -- + Intentionally targeting ActionList.Item for custom border radius. + We accept that the surrounding DOM may change. */ ``` -### Internal CSS usage +#### Internal CSS usage Components may use `data-part` selectors in their own CSS Modules for targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` @@ -142,21 +179,7 @@ with the standardized naming: } ``` -### Coverage requirements - -Every component must provide: - -- **`data-component`** on the root element of every component and public - sub-component -- **`data-part`** on every internal structural element that a consumer might - reasonably need to target (labels, content wrappers, visual slots, action - slots) - -Elements that are purely for layout and have no semantic meaning (spacers, -wrappers that exist only for CSS grid/flex layout) do not require either -attribute. - -### Testing requirements +#### Testing requirements The presence and values of `data-component` and `data-part` attributes must be covered by tests. This can be achieved through: @@ -164,51 +187,92 @@ covered by tests. This can be achieved through: - Unit tests that assert the attributes are present on rendered elements - Snapshot tests that capture the attribute values -Changing a `data-component` or `data-part` value is a **breaking change** and -must follow the standard breaking change process. +#### Documentation on Primer Docs -### Versioning and breaking changes +A dedicated section will be added to Primer Docs explaining what the stable +selectors are intended for and what they are not intended for. -Because `data-component` and `data-part` are **public API**, changes to them -follow [Semantic Versioning](../versioning.md). The table below summarises what -requires which kind of release: +**Intended use cases:** -| Change | semver bump | -| ------------------------------------------------------------------ | ----------- | -| A `data-component` or `data-part` attribute is added to an element | `minor` | -| A `data-component` or `data-part` value is renamed | `major` | -| A `data-component` or `data-part` attribute is removed | `major` | -| An attribute is moved from one element to another | `major` | -| A `data-component` is changed to `data-part` or vice-versa | `major` | +- **Unit tests** — targeting components and slots in test assertions + (e.g., `getByAttribute('data-component', 'ActionList.Item')`) +- **End-to-end testing** — using stable selectors in Playwright or Cypress + locators instead of brittle class names or DOM structure +- **Simple CSS selectors** — directly targeting a component or slot for style + overrides (e.g., `[data-component="Button"] { border-radius: 8px; }`) -**Deprecation path.** Before removing or renaming a value in a major release, -the old value should be deprecated in at least one prior minor release. During -the deprecation window the component must emit a development-mode console -warning (using the existing `warn` / `deprecate` helpers) so consumers have -time to migrate. +**Not intended for:** -The [Migration](#migration) table below captures the full set of renames -planned for the next major release. +- **Complex CSS selector chaining** — sibling selectors (`~`, `+`), child + combinators (`>`), or parent selectors (`:has()`) that depend on DOM structure +- **CSS hacks** — workarounds that rely on the internal layout, nesting, or + ordering of elements within a component +- **Any code that assumes a specific DOM structure** — the DOM tree around a + `data-component` element (its parent, children, siblings, and attributes) is + not part of the public API and may change without notice + +The best solution is always to work alongside Primer for your use case. If you +find yourself needing overly complex selectors or targeting `data-component` +attributes for something bigger than their intended use, chances are there is a +better approach. The Primer team is happy to help, guide, and assist with +whatever your use case may be. The design system is always growing — if we are +not covering your use case, we would love to hear from you, or even better, +accept a contribution! + +### Relationship to CSS Modules and CSS Layers + +While the primary purpose of `data-component` and `data-part` is identification +(testing, tracking, querying), they also serve as stable selectors for CSS +overrides when consumers need to customize appearance. + +In that context, they complement the existing styling architecture: + +- **CSS Modules** provide scoped class names for internal styling. Components + continue to use CSS Module classes for their own styles. +- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer + overrides take precedence over component styles regardless of specificity. +- **`data-component` and `data-part`** provide the stable selectors that + consumers can use to target components and their parts within those overrides. + +Example of a simple, supported override: + +> **Note:** This is an example for demo purposes only. In practice, +> `ActionList.Item` accepts a `className` prop, which is better suited for this +> type of override. You should always prefer the `className` prop over a stable +> selector when possible. + +```css +[data-component='ActionList.Item'] { + border-radius: 8px; +} +``` ### Versioning and breaking changes -Because `data-component` and `data-part` are **public API**, changes to them -follow [Semantic Versioning](../versioning.md). The table below summarises what -requires which kind of release: +The **only** guarantee provided for `data-component` and `data-part` attributes is +that they **exist** and are **tied to the most relevant DOM element** for each +component and its slots. -| Change | semver bump | -| ------------------------------------------------------------------ | ----------- | -| A `data-component` or `data-part` attribute is added to an element | `minor` | -| A `data-component` or `data-part` value is renamed | `major` | -| A `data-component` or `data-part` attribute is removed | `major` | -| An attribute is moved from one element to another | `major` | -| A `data-component` is changed to `data-part` or vice-versa | `major` | +The following aspects are **not** part of the public API and may change at any +time without notice or a major semver bump: -**Deprecation path.** Before removing or renaming a value in a major release, -the old value should be deprecated in at least one prior minor release. During -the deprecation window the component must emit a development-mode console -warning (using the existing `warn` / `deprecate` helpers) so consumers have -time to migrate. +- The specific DOM element a `data-component` or `data-part` attribute is + applied to +- The attributes, parent, or children of that element + +Because of this, consumers should only rely on the **direct targeting** of +these selectors (e.g., `[data-component="ActionList.Item"]`). Chaining +selectors that depend on DOM structure — such as parent, child, or sibling +selectors — is **not supported** and may break without warning. + +| Change | semver bump | +| ------------------------------------------------------------------ | ------------- | +| A `data-component` or `data-part` attribute is added to an element | `minor` | +| A `data-component` or `data-part` value is renamed | `major` | +| A `data-component` or `data-part` attribute is removed | `major` | +| A `data-component` is changed to `data-part` or vice-versa | `major` | +| The DOM element an attribute is applied to changes | `minor` | +| Attributes, parents, or children of the element change | `patch/minor` | The [Migration](#migration) table below captures the full set of renames planned for the next major release. @@ -253,9 +317,13 @@ Components that currently have no attributes on key parts must also be updated. ### Positive -- **Stable selectors for consumers.** Consumers can target any component with - `[data-component="..."]` and any inner part with `[data-part="..."]` — both - are immune to CSS Module hash changes and version upgrades. +- **Stable identifiers for testing.** Consumers can use `data-component` and + `data-part` as reliable locators in unit tests, integration tests, and + end-to-end tests — no more coupling to CSS Module hashes or DOM structure. +- **Enables tracking and monitoring.** Consumers can query the DOM for component + usage metrics and observability without relying on implementation details. +- **Enables JavaScript queries.** Consumers and tests can use + `querySelectorAll('[data-component="ActionList.Item"]')` reliably. - **Clear separation.** `data-component` answers "which component is this?" while `data-part` answers "which part of the component is this?" This makes the DOM self-documenting and avoids overloading a single attribute. @@ -264,18 +332,16 @@ Components that currently have no attributes on key parts must also be updated. - **Scoped slot names.** Because `data-part` values are scoped to their parent `data-component`, names like `Label` or `LeadingVisual` can be reused across components without ambiguity. -- **Enables JavaScript queries.** Consumers and tests can use - `querySelectorAll('[data-component="ActionList.Item"] [data-part="Label"]')` - reliably. -- **Complements CSS Layers.** Together with ADR-021, this gives consumers a - complete, specificity-safe override mechanism. +- **Supports CSS overrides.** Consumers who need to + customize styles have stable selectors to target, complementing CSS Layers + (ADR-021) for a complete override path. ### Negative - **Breaking change for existing consumers.** Anyone currently relying on the - undocumented `data-component` values (e.g., in CSS overrides or - `querySelector` calls) will need to update when values are renamed. This must - be coordinated in a major release. + undocumented `data-component` values (e.g., in tests, tracking code, CSS + overrides, or `querySelector` calls) will need to update when values are + renamed. This must be coordinated in a major release. ## Alternatives @@ -302,7 +368,7 @@ applicable to our architecture. Continue using `data-component` informally without guaranteeing stability. -**Why not chosen:** Consumers are already depending on these attributes for -overrides (as seen in SelectPanel story CSS). Without a stability guarantee, -any refactor can silently break consumer overrides. Formalizing the API -acknowledges the reality and provides a proper contract. +**Why not chosen:** Consumers are already depending on these attributes in +tests, tracking code, and CSS overrides. Without a stability guarantee, any +refactor can silently break consumer code. Formalizing the API acknowledges the +reality and provides a proper contract. From cf7aabd43e35b7023eac29db9c1441e707d237aa Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Thu, 19 Mar 2026 01:07:43 -0400 Subject: [PATCH 08/10] update versioning --- contributor-docs/versioning.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contributor-docs/versioning.md b/contributor-docs/versioning.md index 88b4621c69b..1aee49ba1c8 100644 --- a/contributor-docs/versioning.md +++ b/contributor-docs/versioning.md @@ -70,10 +70,12 @@ For a full list of releases, visit our [releases](https://github.com/primer/reac | | [A component changes its usage of a CSS Custom Property](#a-component-changes-its-usage-of-a-css-custom-property) | potentially `major` | | Accessibility | [A component includes a landmark role](#a-component-includes-a-landmark-role) | potentially `major` | | | [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) | potentially `major` | -| Data attrs | A `data-component` or `data-part` attribute is added ([ADR-023](./adrs/adr-023-data-component-api.md)) | `minor` | -| | A `data-component` or `data-part` value is renamed, removed, or moved to a different element | `major` | -| Data attrs | A `data-component` or `data-part` attribute is added ([ADR-023](./adrs/adr-023-data-component-api.md)) | `minor` | -| | A `data-component` or `data-part` value is renamed, removed, or moved to a different element | `major` | +| Data attrs | A `data-component` or `data-part` attribute is added to an element ([ADR-023](./adrs/adr-023-stable-selectors-api.md)) | `minor` | +| | A `data-component` or `data-part` value is renamed | `major` | +| | A `data-component` or `data-part` attribute is removed | `major` | +| | A `data-component` is changed to `data-part` or vice-versa | `major` | +| | The DOM element an attribute is applied to changes | `minor` | +| | Attributes, parents, or children of the element change | `patch/minor` | ## Reference From e6753179712bc27ae0ec27d41adc2d95291a2aa5 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Mon, 23 Mar 2026 22:44:29 -0400 Subject: [PATCH 09/10] fix verbiage around slots/parts/internals --- .../adrs/adr-023-stable-selectors-api.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contributor-docs/adrs/adr-023-stable-selectors-api.md b/contributor-docs/adrs/adr-023-stable-selectors-api.md index 998b5d11de4..c01e3346dcb 100644 --- a/contributor-docs/adrs/adr-023-stable-selectors-api.md +++ b/contributor-docs/adrs/adr-023-stable-selectors-api.md @@ -112,8 +112,8 @@ data-part="PartName" → inner part within a component ``` - Slot names are **scoped to their parent component** — a `Label` slot inside - `ActionList.Item` is distinct from a `Label` slot inside `Button` because + `data-part` names are **scoped to their parent component** — a `Label` part inside + `ActionList.Item` is distinct from a `Label` part inside `Button` because they exist within different `data-component` boundaries. 4. **State and modifier attributes remain separate.** `data-component` and @@ -127,10 +127,10 @@ data-part="PartName" → inner part within a component ``` -#### `data-component` and `data-part` on all components and slots +#### `data-component` and `data-part` on all components and appropriate internals All Primer components will receive a `data-component` attribute on their root -element, and all meaningful slots will receive a `data-part` attribute. These +element, and all meaningful internal components will receive a `data-part` attribute. These attributes serve as the stable identifiers that consumers can use for targeting, ensuring full coverage across the library. @@ -194,11 +194,11 @@ selectors are intended for and what they are not intended for. **Intended use cases:** -- **Unit tests** — targeting components and slots in test assertions +- **Unit tests** — targeting components and internals in test assertions (e.g., `getByAttribute('data-component', 'ActionList.Item')`) - **End-to-end testing** — using stable selectors in Playwright or Cypress locators instead of brittle class names or DOM structure -- **Simple CSS selectors** — directly targeting a component or slot for style +- **Simple CSS selectors** — directly targeting a component or part for style overrides (e.g., `[data-component="Button"] { border-radius: 8px; }`) **Not intended for:** @@ -251,7 +251,7 @@ Example of a simple, supported override: The **only** guarantee provided for `data-component` and `data-part` attributes is that they **exist** and are **tied to the most relevant DOM element** for each -component and its slots. +component and its internals. The following aspects are **not** part of the public API and may change at any time without notice or a major semver bump: @@ -329,7 +329,7 @@ Components that currently have no attributes on key parts must also be updated. the DOM self-documenting and avoids overloading a single attribute. - **Consistent naming.** A single convention replaces four inconsistent patterns, making the codebase easier to learn and maintain. -- **Scoped slot names.** Because `data-part` values are scoped to their parent +- **Scoped part names.** Because `data-part` values are scoped to their parent `data-component`, names like `Label` or `LeadingVisual` can be reused across components without ambiguity. - **Supports CSS overrides.** Consumers who need to From bb18a24bd00281c43e9fd77b0e3bbff0fbe8de20 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Thu, 26 Mar 2026 21:48:53 -0400 Subject: [PATCH 10/10] fix: update status and attributes in ADR-023 for stable selectors --- .../adrs/adr-023-stable-selectors-api.md | 169 ++++++++---------- contributor-docs/versioning.md | 7 +- 2 files changed, 77 insertions(+), 99 deletions(-) diff --git a/contributor-docs/adrs/adr-023-stable-selectors-api.md b/contributor-docs/adrs/adr-023-stable-selectors-api.md index c01e3346dcb..bca1a928831 100644 --- a/contributor-docs/adrs/adr-023-stable-selectors-api.md +++ b/contributor-docs/adrs/adr-023-stable-selectors-api.md @@ -6,7 +6,7 @@ | Stage | State | | -------------- | ----------- | -| Status | Proposed ❓ | +| Status | Accepted ✅ | | Implementation | Pending ⚠️ | ## Context @@ -49,17 +49,16 @@ without notice and coverage is incomplete — many component parts have no ## Decision -Establish two **public, stable data attributes** that act as reliable -identifiers for Primer components and their parts in the DOM. Think of these as -built-in test IDs — stable, predictable anchors that consumers can use for +Establish a **public, stable data attribute** that acts as a reliable +identifier for Primer components in the DOM. Think of this as +a built-in test ID — a stable, predictable anchor that consumers can use for testing, tracking, monitoring, and querying without coupling to internal DOM structure. -- **`data-component`** — identifies the root element of a component or - sub-component. -- **`data-part`** — identifies an inner structural part within a component. +- **`data-component`** — identifies the root element of a component, + sub-component, or internal part. -These attributes are primarily intended for: +This attribute is primarily intended for: 1. **Testing** — use them as locators in unit tests, integration tests, and end-to-end tests instead of brittle class names or DOM paths. @@ -75,11 +74,10 @@ These attributes are primarily intended for: #### Naming convention -All values use PascalCase. The two attributes serve distinct roles: +All values use PascalCase: ``` -data-component="ComponentName" → root element of a component or sub-component -data-part="PartName" → inner part within a component +data-component="ComponentName" → root element of a component, sub-component, or internal part ``` ##### Rules @@ -98,58 +96,44 @@ data-part="PartName" → inner part within a component
    • ``` - Note: a sub-component root uses `data-component`, not `data-part`, because it - is itself a component — it has its own props, its own identity, and may - contain its own slots. - 3. **Inner structural parts** (DOM elements that are not exposed as a sub-component but represent a meaningful part of the structure) get - `data-part` with a PascalCase name describing the part. + `data-component` with the parent component name and a PascalCase part name. ```html - monalisa - ... - + monalisa + ... + ``` - `data-part` names are **scoped to their parent component** — a `Label` part inside - `ActionList.Item` is distinct from a `Label` part inside `Button` because - they exist within different `data-component` boundaries. + Part names are scoped to their parent component via the naming convention — + `ActionList.Item.Label` is distinct from `Button.Label`. -4. **State and modifier attributes remain separate.** `data-component` and - `data-part` identify _what_ an element is. Existing attributes like +4. **State and modifier attributes remain separate.** `data-component` + identifies _what_ an element is. Existing attributes like `data-variant`, `data-size`, and `data-loading` describe the _state_ of that element. These concerns must not be mixed. ```html
    • - Delete file + Delete file
    • ``` -#### `data-component` and `data-part` on all components and appropriate internals +#### `data-component` on all components and appropriate internals -All Primer components will receive a `data-component` attribute on their root -element, and all meaningful internal components will receive a `data-part` attribute. These -attributes serve as the stable identifiers that consumers can use for targeting, +All Primer components, slots and meaningful parts will receive a `data-component` attribute on their root. This +attribute serves as the stable identifier that consumers can use for targeting, ensuring full coverage across the library. -Every component must provide: - -- **`data-component`** on the root element of every component and public - sub-component -- **`data-part`** on every internal structural element that a consumer might - reasonably need to target (labels, content wrappers, visual slots, action - slots) - Elements that are purely for layout and have no semantic meaning (spacers, -wrappers that exist only for CSS grid/flex layout) do not require either +wrappers that exist only for CSS grid/flex layout) do not require this attribute. #### ESLint rule in `eslint-plugin-primer-react` A new ESLint rule will be added to `eslint-plugin-primer-react` that **warns** -whenever a `data-component` or `data-part` selector is used in consumer code. The warning will +whenever a `data-component` selector is used in consumer code. The warning will inform consumers of the implications of relying on these selectors — namely that the surrounding DOM structure, attributes, parents, and children of the targeted element are not guaranteed to be stable. @@ -168,20 +152,20 @@ component's DOM. #### Internal CSS usage -Components may use `data-part` selectors in their own CSS Modules for targeting +Components may use `data-component` selectors in their own CSS Modules for targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` with the standardized naming: ```css /* ButtonBase.module.css */ -& :where([data-part='LeadingVisual']) { +& :where([data-component='Button.LeadingVisual']) { color: var(--button-leadingVisual-fgColor); } ``` #### Testing requirements -The presence and values of `data-component` and `data-part` attributes must be +The presence and values of `data-component` attributes must be covered by tests. This can be achieved through: - Unit tests that assert the attributes are present on rendered elements @@ -221,17 +205,17 @@ accept a contribution! ### Relationship to CSS Modules and CSS Layers -While the primary purpose of `data-component` and `data-part` is identification -(testing, tracking, querying), they also serve as stable selectors for CSS +While the primary purpose of `data-component` is identification +(testing, tracking, querying), it also serves as a stable selector for CSS overrides when consumers need to customize appearance. -In that context, they complement the existing styling architecture: +In that context, it complements the existing styling architecture: - **CSS Modules** provide scoped class names for internal styling. Components continue to use CSS Module classes for their own styles. - **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer overrides take precedence over component styles regardless of specificity. -- **`data-component` and `data-part`** provide the stable selectors that +- **`data-component`** provides the stable selectors that consumers can use to target components and their parts within those overrides. Example of a simple, supported override: @@ -249,14 +233,14 @@ Example of a simple, supported override: ### Versioning and breaking changes -The **only** guarantee provided for `data-component` and `data-part` attributes is +The **only** guarantee provided for `data-component` attributes is that they **exist** and are **tied to the most relevant DOM element** for each component and its internals. The following aspects are **not** part of the public API and may change at any time without notice or a major semver bump: -- The specific DOM element a `data-component` or `data-part` attribute is +- The specific DOM element a `data-component` attribute is applied to - The attributes, parent, or children of that element @@ -265,51 +249,49 @@ these selectors (e.g., `[data-component="ActionList.Item"]`). Chaining selectors that depend on DOM structure — such as parent, child, or sibling selectors — is **not supported** and may break without warning. -| Change | semver bump | -| ------------------------------------------------------------------ | ------------- | -| A `data-component` or `data-part` attribute is added to an element | `minor` | -| A `data-component` or `data-part` value is renamed | `major` | -| A `data-component` or `data-part` attribute is removed | `major` | -| A `data-component` is changed to `data-part` or vice-versa | `major` | -| The DOM element an attribute is applied to changes | `minor` | -| Attributes, parents, or children of the element change | `patch/minor` | +| Change | semver bump | +| ------------------------------------------------------ | ------------- | +| A `data-component` attribute is added to an element | `minor` | +| A `data-component` value is renamed | `major` | +| A `data-component` attribute is removed | `major` | +| The DOM element an attribute is applied to changes | `minor` | +| Attributes, parents, or children of the element change | `patch/minor` | The [Migration](#migration) table below captures the full set of renames planned for the next major release. ### Migration -Existing `data-component` values must be migrated to the new convention. Inner -parts move from `data-component` to `data-part` with simplified names (since -they are scoped to their parent component). This migration is a breaking change -and should be coordinated as part of a major release. - -| Current attr | Current value | New attr | New value | -| ---------------- | --------------------------------------- | ---------------- | --------------------------- | -| `data-component` | `buttonContent` | `data-part` | `Content` | -| `data-component` | `text` (in Button) | `data-part` | `Label` | -| `data-component` | `leadingVisual` (in Button) | `data-part` | `LeadingVisual` | -| `data-component` | `trailingVisual` (in Button) | `data-part` | `TrailingVisual` | -| `data-component` | `trailingAction` (in Button) | `data-part` | `TrailingAction` | -| `data-component` | `ButtonCounter` | `data-part` | `Counter` | -| `data-component` | `PH_LeadingAction` | `data-part` | `LeadingAction` | -| `data-component` | `PH_Breadcrumbs` | `data-part` | `Breadcrumbs` | -| `data-component` | `PH_LeadingVisual` | `data-part` | `LeadingVisual` | -| `data-component` | `PH_Title` | `data-part` | `Title` | -| `data-component` | `PH_TrailingVisual` | `data-part` | `TrailingVisual` | -| `data-component` | `PH_TrailingAction` | `data-part` | `TrailingAction` | -| `data-component` | `PH_Actions` | `data-part` | `Actions` | -| `data-component` | `PH_Navigation` | `data-part` | `Navigation` | -| `data-component` | `TitleArea` | `data-part` | `TitleArea` | -| `data-component` | `GroupHeadingWrap` | `data-component` | `ActionList.GroupHeading` | -| `data-component` | `ActionList.Item--DividerContainer` | `data-part` | `SubContent` | -| `data-component` | `icon` (in UnderlineTabbedInterface) | `data-part` | `Icon` | -| `data-component` | `text` (in UnderlineTabbedInterface) | `data-part` | `Label` | -| `data-component` | `counter` (in UnderlineTabbedInterface) | `data-part` | `Counter` | -| `data-component` | `multilineContainer` | `data-part` | `Container` | -| `data-component` | `input` (in TextInput) | `data-part` | `Input` | -| `data-component` | `AnchoredOverlay` | `data-component` | `AnchoredOverlay` | -| `data-component` | `ActionBar.VerticalDivider` | `data-component` | `ActionBar.VerticalDivider` | +Existing `data-component` values must be migrated to the new convention. This +migration is a breaking change and should be coordinated as part of a major +release. + +| Current value | New value | +| --------------------------------------- | ---------------------------- | +| `buttonContent` | `Button.Content` | +| `text` (in Button) | `Button.Label` | +| `leadingVisual` (in Button) | `Button.LeadingVisual` | +| `trailingVisual` (in Button) | `Button.TrailingVisual` | +| `trailingAction` (in Button) | `Button.TrailingAction` | +| `ButtonCounter` | `Button.Counter` | +| `PH_LeadingAction` | `PageHeader.LeadingAction` | +| `PH_Breadcrumbs` | `PageHeader.Breadcrumbs` | +| `PH_LeadingVisual` | `PageHeader.LeadingVisual` | +| `PH_Title` | `PageHeader.Title` | +| `PH_TrailingVisual` | `PageHeader.TrailingVisual` | +| `PH_TrailingAction` | `PageHeader.TrailingAction` | +| `PH_Actions` | `PageHeader.Actions` | +| `PH_Navigation` | `PageHeader.Navigation` | +| `TitleArea` | `PageHeader.TitleArea` | +| `GroupHeadingWrap` | `ActionList.GroupHeading` | +| `ActionList.Item--DividerContainer` | `ActionList.Item.SubContent` | +| `icon` (in UnderlineTabbedInterface) | `UnderlineNav.Item.Icon` | +| `text` (in UnderlineTabbedInterface) | `UnderlineNav.Item.Label` | +| `counter` (in UnderlineTabbedInterface) | `UnderlineNav.Item.Counter` | +| `multilineContainer` | `TextInput.Container` | +| `input` (in TextInput) | `TextInput.Input` | +| `AnchoredOverlay` | `AnchoredOverlay` | +| `ActionBar.VerticalDivider` | `ActionBar.VerticalDivider` | Components that currently have no attributes on key parts must also be updated. @@ -317,21 +299,18 @@ Components that currently have no attributes on key parts must also be updated. ### Positive -- **Stable identifiers for testing.** Consumers can use `data-component` and - `data-part` as reliable locators in unit tests, integration tests, and +- **Stable identifiers for testing.** Consumers can use `data-component` + as reliable locators in unit tests, integration tests, and end-to-end tests — no more coupling to CSS Module hashes or DOM structure. - **Enables tracking and monitoring.** Consumers can query the DOM for component usage metrics and observability without relying on implementation details. - **Enables JavaScript queries.** Consumers and tests can use `querySelectorAll('[data-component="ActionList.Item"]')` reliably. -- **Clear separation.** `data-component` answers "which component is this?" - while `data-part` answers "which part of the component is this?" This makes - the DOM self-documenting and avoids overloading a single attribute. - **Consistent naming.** A single convention replaces four inconsistent patterns, making the codebase easier to learn and maintain. -- **Scoped part names.** Because `data-part` values are scoped to their parent - `data-component`, names like `Label` or `LeadingVisual` can be reused across - components without ambiguity. +- **Unified approach.** All elements — whether root components, sub-components, + or internal parts — use `data-component`, providing a simple and consistent + mental model. - **Supports CSS overrides.** Consumers who need to customize styles have stable selectors to target, complementing CSS Layers (ADR-021) for a complete override path. diff --git a/contributor-docs/versioning.md b/contributor-docs/versioning.md index 1aee49ba1c8..ae61443768a 100644 --- a/contributor-docs/versioning.md +++ b/contributor-docs/versioning.md @@ -70,10 +70,9 @@ For a full list of releases, visit our [releases](https://github.com/primer/reac | | [A component changes its usage of a CSS Custom Property](#a-component-changes-its-usage-of-a-css-custom-property) | potentially `major` | | Accessibility | [A component includes a landmark role](#a-component-includes-a-landmark-role) | potentially `major` | | | [A component no longer includes a landmark role](#a-component-no-longer-includes-a-landmark-role) | potentially `major` | -| Data attrs | A `data-component` or `data-part` attribute is added to an element ([ADR-023](./adrs/adr-023-stable-selectors-api.md)) | `minor` | -| | A `data-component` or `data-part` value is renamed | `major` | -| | A `data-component` or `data-part` attribute is removed | `major` | -| | A `data-component` is changed to `data-part` or vice-versa | `major` | +| Data attrs | A `data-component` attribute is added to an element ([ADR-023](./adrs/adr-023-stable-selectors-api.md)) | `minor` | +| | A `data-component` value is renamed | `major` | +| | A `data-component` attribute is removed | `major` | | | The DOM element an attribute is applied to changes | `minor` | | | Attributes, parents, or children of the element change | `patch/minor` |