|
1 | | -# Public `data-component` API for targeting component parts |
| 1 | +# Public `data-component` and `data-slot` API for targeting component parts |
2 | 2 |
|
3 | 3 | 📆 Date: 2026-02-20 |
4 | 4 |
|
5 | 5 | ## Status |
6 | 6 |
|
7 | | -| Stage | State | |
8 | | -| -------------- | -------------- | |
9 | | -| Status | Proposed ❓ | |
10 | | -| Implementation | | |
| 7 | +| Stage | State | |
| 8 | +| -------------- | ----------- | |
| 9 | +| Status | Proposed ❓ | |
| 10 | +| Implementation | | |
11 | 11 |
|
12 | 12 | ## Context |
13 | 13 |
|
@@ -43,162 +43,184 @@ without notice and coverage is incomplete — many component parts have no |
43 | 43 |
|
44 | 44 | ## Decision |
45 | 45 |
|
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. |
49 | 52 |
|
50 | 53 | ### Naming convention |
51 | 54 |
|
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: |
54 | 56 |
|
55 | 57 | ``` |
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 |
58 | 60 | ``` |
59 | 61 |
|
60 | 62 | #### Rules |
61 | 63 |
|
62 | | -1. **Root components** use their React component name in PascalCase. |
| 64 | +1. **Root components** get `data-component` with their React component name. |
63 | 65 |
|
64 | 66 | ```html |
65 | 67 | <ul data-component="ActionList"></ul> |
66 | 68 | ``` |
67 | 69 |
|
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 |
69 | 71 | consumers write `<ActionList.Item>`, the DOM element gets |
70 | 72 | `data-component="ActionList.Item"`. |
71 | 73 |
|
72 | 74 | ```html |
73 | 75 | <li data-component="ActionList.Item"></li> |
74 | 76 | ``` |
75 | 77 |
|
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. |
79 | 85 |
|
80 | 86 | ```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> |
84 | 90 | ``` |
85 | 91 |
|
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 |
88 | 98 | `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. |
90 | 100 |
|
91 | 101 | ```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> |
93 | 105 | ``` |
94 | 106 |
|
95 | 107 | ### Relationship to CSS Modules and CSS Layers |
96 | 108 |
|
97 | | -`data-component` complements the existing styling architecture: |
| 109 | +`data-component` and `data-slot` complement the existing styling architecture: |
98 | 110 |
|
99 | 111 | - **CSS Modules** provide scoped class names for internal styling. Components |
100 | 112 | continue to use CSS Module classes for their own styles. |
101 | 113 | - **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer |
102 | 114 | 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. |
105 | 117 |
|
106 | 118 | Together, these three mechanisms give consumers a complete override path: |
107 | 119 |
|
108 | 120 | ```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'] { |
111 | 128 | font-weight: 600; |
112 | 129 | } |
113 | 130 | ``` |
114 | 131 |
|
115 | 132 | ### Internal CSS usage |
116 | 133 |
|
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: |
120 | 137 |
|
121 | 138 | ```css |
122 | 139 | /* ButtonBase.module.css */ |
123 | | -& :where([data-component='Button.LeadingVisual']) { |
| 140 | +& :where([data-slot='LeadingVisual']) { |
124 | 141 | color: var(--button-leadingVisual-fgColor); |
125 | 142 | } |
126 | 143 | ``` |
127 | 144 |
|
128 | 145 | ### Coverage requirements |
129 | 146 |
|
130 | | -Every component must provide `data-component` on: |
| 147 | +Every component must provide: |
131 | 148 |
|
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) |
136 | 154 |
|
137 | 155 | 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. |
140 | 158 |
|
141 | 159 | ### Testing requirements |
142 | 160 |
|
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: |
145 | 163 |
|
146 | | -- Unit tests that assert `data-component` is present on rendered elements |
| 164 | +- Unit tests that assert the attributes are present on rendered elements |
147 | 165 | - Snapshot tests that capture the attribute values |
148 | 166 |
|
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. |
151 | 169 |
|
152 | 170 | ### Migration |
153 | 171 |
|
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. |
187 | 205 |
|
188 | 206 | ## Consequences |
189 | 207 |
|
190 | 208 | ### Positive |
191 | 209 |
|
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. |
195 | 216 | - **Consistent naming.** A single convention replaces four inconsistent patterns, |
196 | 217 | 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. |
200 | 221 | - **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. |
202 | 224 | - **Complements CSS Layers.** Together with ADR-021, this gives consumers a |
203 | 225 | complete, specificity-safe override mechanism. |
204 | 226 |
|
|
0 commit comments