From 3dd34d509a0671150bf4f5dd9ace121f5888b6eb Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Fri, 20 Feb 2026 14:46:29 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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