Skip to content

Commit a5adc10

Browse files
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>
1 parent 3dd34d5 commit a5adc10

1 file changed

Lines changed: 107 additions & 85 deletions

File tree

contributor-docs/adrs/adr-023-data-component-api.md

Lines changed: 107 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
# Public `data-component` API for targeting component parts
1+
# Public `data-component` and `data-slot` API for targeting component parts
22

33
📆 Date: 2026-02-20
44

55
## Status
66

7-
| Stage | State |
8-
| -------------- | -------------- |
9-
| Status | Proposed ❓ |
10-
| Implementation | |
7+
| Stage | State |
8+
| -------------- | ----------- |
9+
| Status | Proposed ❓ |
10+
| Implementation | |
1111

1212
## Context
1313

@@ -43,162 +43,184 @@ without notice and coverage is incomplete — many component parts have no
4343

4444
## Decision
4545

46-
Establish `data-component` as a **public, stable API** for identifying component
47-
parts in the DOM. Every DOM element that represents a component or a meaningful
48-
structural part of a component must include a `data-component` attribute.
46+
Establish two **public, stable data attributes** for identifying components and
47+
their parts in the DOM:
48+
49+
- **`data-component`** — identifies the root element of a component or
50+
sub-component.
51+
- **`data-slot`** — identifies an inner structural part within a component.
4952

5053
### Naming convention
5154

52-
Values follow **dot notation mirroring the React component API**, using
53-
PascalCase throughout:
55+
All values use PascalCase. The two attributes serve distinct roles:
5456

5557
```
56-
data-component="ComponentName" → root element
57-
data-component="ComponentName.PartName" → sub-component or internal part
58+
data-component="ComponentName" → root element of a component or sub-component
59+
data-slot="PartName" → inner part within a component
5860
```
5961

6062
#### Rules
6163

62-
1. **Root components** use their React component name in PascalCase.
64+
1. **Root components** get `data-component` with their React component name.
6365

6466
```html
6567
<ul data-component="ActionList"></ul>
6668
```
6769

68-
2. **Public sub-components** use dot notation matching the React API. If
70+
2. **Public sub-components** get `data-component` matching the React API. If
6971
consumers write `<ActionList.Item>`, the DOM element gets
7072
`data-component="ActionList.Item"`.
7173

7274
```html
7375
<li data-component="ActionList.Item"></li>
7476
```
7577

76-
3. **Internal structural parts** (DOM elements that are not exposed as a
77-
sub-component but represent a meaningful part of the structure) use the parent
78-
component name followed by a PascalCase part name in dot notation.
78+
Note: a sub-component root uses `data-component`, not `data-slot`, because it
79+
is itself a component — it has its own props, its own identity, and may
80+
contain its own slots.
81+
82+
3. **Inner structural parts** (DOM elements that are not exposed as a
83+
sub-component but represent a meaningful part of the structure) get
84+
`data-slot` with a PascalCase name describing the part.
7985

8086
```html
81-
<span data-component="ActionList.ItemLabel">
82-
<span data-component="ActionList.ItemContent"> <span data-component="Button.Content"></span></span
83-
></span>
87+
<span data-slot="Label">monalisa</span>
88+
<span data-slot="Content">...</span>
89+
<span data-slot="LeadingVisual"><img /></span>
8490
```
8591

86-
4. **State and modifier attributes remain separate.** The `data-component`
87-
attribute identifies _what_ a part is. Existing attributes like
92+
Slot names are **scoped to their parent component** — a `Label` slot inside
93+
`ActionList.Item` is distinct from a `Label` slot inside `Button` because
94+
they exist within different `data-component` boundaries.
95+
96+
4. **State and modifier attributes remain separate.** `data-component` and
97+
`data-slot` identify _what_ an element is. Existing attributes like
8898
`data-variant`, `data-size`, and `data-loading` describe the _state_ of that
89-
part. These concerns must not be mixed.
99+
element. These concerns must not be mixed.
90100

91101
```html
92-
<li data-component="ActionList.Item" data-variant="danger" data-active="true"></li>
102+
<li data-component="ActionList.Item" data-variant="danger" data-active="true">
103+
<span data-slot="Label">Delete file</span>
104+
</li>
93105
```
94106

95107
### Relationship to CSS Modules and CSS Layers
96108

97-
`data-component` complements the existing styling architecture:
109+
`data-component` and `data-slot` complement the existing styling architecture:
98110

99111
- **CSS Modules** provide scoped class names for internal styling. Components
100112
continue to use CSS Module classes for their own styles.
101113
- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer
102114
overrides take precedence over component styles regardless of specificity.
103-
- **`data-component`** provides the stable selectors that consumers use to
104-
target parts within those overrides.
115+
- **`data-component` and `data-slot`** provide the stable selectors that
116+
consumers use to target components and their parts within those overrides.
105117

106118
Together, these three mechanisms give consumers a complete override path:
107119

108120
```css
109-
/* Consumer override — wins over component styles thanks to CSS layers */
110-
[data-component='ActionList.ItemLabel'] {
121+
/* Target a component */
122+
[data-component='ActionList.Item'] {
123+
border-radius: 8px;
124+
}
125+
126+
/* Target a slot within a component */
127+
[data-component='ActionList.Item'] [data-slot='Label'] {
111128
font-weight: 600;
112129
}
113130
```
114131

115132
### Internal CSS usage
116133

117-
Components may use `data-component` selectors in their own CSS Modules for
118-
targeting child parts. This replaces ad-hoc patterns like bare `[data-component='text']` with the
119-
standardized naming:
134+
Components may use `data-slot` selectors in their own CSS Modules for targeting
135+
child parts. This replaces ad-hoc patterns like bare `[data-component='text']`
136+
with the standardized naming:
120137

121138
```css
122139
/* ButtonBase.module.css */
123-
& :where([data-component='Button.LeadingVisual']) {
140+
& :where([data-slot='LeadingVisual']) {
124141
color: var(--button-leadingVisual-fgColor);
125142
}
126143
```
127144

128145
### Coverage requirements
129146

130-
Every component must provide `data-component` on:
147+
Every component must provide:
131148

132-
1. The root element
133-
2. Every public sub-component element
134-
3. Every internal structural element that a consumer might reasonably need to
135-
target (labels, content wrappers, visual slots, action slots)
149+
- **`data-component`** on the root element of every component and public
150+
sub-component
151+
- **`data-slot`** on every internal structural element that a consumer might
152+
reasonably need to target (labels, content wrappers, visual slots, action
153+
slots)
136154

137155
Elements that are purely for layout and have no semantic meaning (spacers,
138-
wrappers that exist only for CSS grid/flex layout) do not require
139-
`data-component`.
156+
wrappers that exist only for CSS grid/flex layout) do not require either
157+
attribute.
140158

141159
### Testing requirements
142160

143-
The presence and value of `data-component` attributes must be covered by tests.
144-
This can be achieved through:
161+
The presence and values of `data-component` and `data-slot` attributes must be
162+
covered by tests. This can be achieved through:
145163

146-
- Unit tests that assert `data-component` is present on rendered elements
164+
- Unit tests that assert the attributes are present on rendered elements
147165
- Snapshot tests that capture the attribute values
148166

149-
Changing a `data-component` value is a **breaking change** and must follow the
150-
standard breaking change process.
167+
Changing a `data-component` or `data-slot` value is a **breaking change** and
168+
must follow the standard breaking change process.
151169

152170
### Migration
153171

154-
Existing `data-component` values must be migrated to the new convention. This
155-
migration is a breaking change and should be coordinated as part of a major
156-
release. The following values need to change:
157-
158-
| Current value | New value |
159-
| --------------------------------------- | --------------------------- |
160-
| `buttonContent` | `Button.Content` |
161-
| `text` (in Button) | `Button.Label` |
162-
| `leadingVisual` (in Button) | `Button.LeadingVisual` |
163-
| `trailingVisual` (in Button) | `Button.TrailingVisual` |
164-
| `trailingAction` (in Button) | `Button.TrailingAction` |
165-
| `ButtonCounter` | `Button.Counter` |
166-
| `PH_LeadingAction` | `PageHeader.LeadingAction` |
167-
| `PH_Breadcrumbs` | `PageHeader.Breadcrumbs` |
168-
| `PH_LeadingVisual` | `PageHeader.LeadingVisual` |
169-
| `PH_Title` | `PageHeader.Title` |
170-
| `PH_TrailingVisual` | `PageHeader.TrailingVisual` |
171-
| `PH_TrailingAction` | `PageHeader.TrailingAction` |
172-
| `PH_Actions` | `PageHeader.Actions` |
173-
| `PH_Navigation` | `PageHeader.Navigation` |
174-
| `TitleArea` | `PageHeader.TitleArea` |
175-
| `GroupHeadingWrap` | `ActionList.GroupHeading` |
176-
| `ActionList.Item--DividerContainer` | `ActionList.ItemSubContent` |
177-
| `icon` (in UnderlineTabbedInterface) | `UnderlineNav.Icon` |
178-
| `text` (in UnderlineTabbedInterface) | `UnderlineNav.Label` |
179-
| `counter` (in UnderlineTabbedInterface) | `UnderlineNav.Counter` |
180-
| `multilineContainer` | `SkeletonText.Container` |
181-
| `input` (in TextInput) | `TextInput.Input` |
182-
| `AnchoredOverlay` (no dot) | `AnchoredOverlay` |
183-
| `ActionBar.VerticalDivider` | `ActionBar.VerticalDivider` |
184-
185-
Components that currently have no `data-component` on key parts must also be
186-
updated to add them.
172+
Existing `data-component` values must be migrated to the new convention. Inner
173+
parts move from `data-component` to `data-slot` with simplified names (since
174+
they are scoped to their parent component). This migration is a breaking change
175+
and should be coordinated as part of a major release.
176+
177+
| Current attr | Current value | New attr | New value |
178+
| ---------------- | --------------------------------------- | ---------------- | --------------------------- |
179+
| `data-component` | `buttonContent` | `data-slot` | `Content` |
180+
| `data-component` | `text` (in Button) | `data-slot` | `Label` |
181+
| `data-component` | `leadingVisual` (in Button) | `data-slot` | `LeadingVisual` |
182+
| `data-component` | `trailingVisual` (in Button) | `data-slot` | `TrailingVisual` |
183+
| `data-component` | `trailingAction` (in Button) | `data-slot` | `TrailingAction` |
184+
| `data-component` | `ButtonCounter` | `data-slot` | `Counter` |
185+
| `data-component` | `PH_LeadingAction` | `data-slot` | `LeadingAction` |
186+
| `data-component` | `PH_Breadcrumbs` | `data-slot` | `Breadcrumbs` |
187+
| `data-component` | `PH_LeadingVisual` | `data-slot` | `LeadingVisual` |
188+
| `data-component` | `PH_Title` | `data-slot` | `Title` |
189+
| `data-component` | `PH_TrailingVisual` | `data-slot` | `TrailingVisual` |
190+
| `data-component` | `PH_TrailingAction` | `data-slot` | `TrailingAction` |
191+
| `data-component` | `PH_Actions` | `data-slot` | `Actions` |
192+
| `data-component` | `PH_Navigation` | `data-slot` | `Navigation` |
193+
| `data-component` | `TitleArea` | `data-slot` | `TitleArea` |
194+
| `data-component` | `GroupHeadingWrap` | `data-component` | `ActionList.GroupHeading` |
195+
| `data-component` | `ActionList.Item--DividerContainer` | `data-slot` | `SubContent` |
196+
| `data-component` | `icon` (in UnderlineTabbedInterface) | `data-slot` | `Icon` |
197+
| `data-component` | `text` (in UnderlineTabbedInterface) | `data-slot` | `Label` |
198+
| `data-component` | `counter` (in UnderlineTabbedInterface) | `data-slot` | `Counter` |
199+
| `data-component` | `multilineContainer` | `data-slot` | `Container` |
200+
| `data-component` | `input` (in TextInput) | `data-slot` | `Input` |
201+
| `data-component` | `AnchoredOverlay` | `data-component` | `AnchoredOverlay` |
202+
| `data-component` | `ActionBar.VerticalDivider` | `data-component` | `ActionBar.VerticalDivider` |
203+
204+
Components that currently have no attributes on key parts must also be updated.
187205

188206
## Consequences
189207

190208
### Positive
191209

192-
- **Stable selectors for consumers.** Consumers can target any part of a
193-
component using `[data-component="..."]` selectors that are immune to CSS
194-
Module hash changes and version upgrades.
210+
- **Stable selectors for consumers.** Consumers can target any component with
211+
`[data-component="..."]` and any inner part with `[data-slot="..."]` — both
212+
are immune to CSS Module hash changes and version upgrades.
213+
- **Clear separation.** `data-component` answers "which component is this?"
214+
while `data-slot` answers "which part of the component is this?" This makes
215+
the DOM self-documenting and avoids overloading a single attribute.
195216
- **Consistent naming.** A single convention replaces four inconsistent patterns,
196217
making the codebase easier to learn and maintain.
197-
- **Self-documenting.** Inspecting any element in DevTools immediately reveals
198-
what component and part it belongs to — the values map directly to the React
199-
API.
218+
- **Scoped slot names.** Because `data-slot` values are scoped to their parent
219+
`data-component`, names like `Label` or `LeadingVisual` can be reused across
220+
components without ambiguity.
200221
- **Enables JavaScript queries.** Consumers and tests can use
201-
`querySelectorAll('[data-component="ActionList.Item"]')` reliably.
222+
`querySelectorAll('[data-component="ActionList.Item"] [data-slot="Label"]')`
223+
reliably.
202224
- **Complements CSS Layers.** Together with ADR-021, this gives consumers a
203225
complete, specificity-safe override mechanism.
204226

0 commit comments

Comments
 (0)