Skip to content

Commit 176b2b5

Browse files
committed
update stable selectors api
1 parent a33b0d8 commit 176b2b5

1 file changed

Lines changed: 169 additions & 103 deletions

File tree

contributor-docs/adrs/adr-023-data-component-api.md renamed to contributor-docs/adrs/adr-023-stable-selectors-api.md

Lines changed: 169 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Public `data-component` and `data-part` API for targeting component parts
1+
# Stable identifiers for Primer components
22

33
📆 Date: 2026-02-20
44

@@ -7,23 +7,29 @@
77
| Stage | State |
88
| -------------- | ----------- |
99
| Status | Proposed ❓ |
10-
| Implementation | |
10+
| Implementation | Pending ⚠️ |
1111

1212
## Context
1313

14-
Primer React uses [CSS Modules](https://github.com/css-modules/css-modules) for
15-
styling. CSS Modules generate hashed class names (e.g.,
16-
`prc-ActionList-Item-cBBI`) that are not stable across versions. This makes it
17-
impossible for consumers to reliably target internal parts of a component for
18-
style overrides using class selectors.
19-
20-
Consumers need the ability to target component parts for legitimate use cases:
21-
22-
- Customizing the appearance of specific parts (e.g., hiding a trailing visual,
23-
changing the font weight of a label)
24-
- Querying parts of a component in JavaScript (e.g., finding all selected items
25-
in an action list)
26-
- Writing integration tests that target stable selectors
14+
Primer React components do not expose stable identifiers for their rendered DOM
15+
elements. Consumers who need to reference a specific component or slot in the
16+
DOM — for testing, tracking, monitoring, or querying — have no reliable way to
17+
do so.
18+
19+
This creates problems across several areas:
20+
21+
- **Testing.** Unit and end-to-end tests resort to brittle selectors like CSS
22+
Module hashes (`prc-ActionList-Item-cBBI`), DOM structure assumptions, or
23+
text content matching — all of which break silently when Primer is updated.
24+
- **Tracking and monitoring.** Consumers that want to measure how components are
25+
used in production (e.g., counting how many ActionList items appear on a page)
26+
have no stable hook to query against.
27+
- **JavaScript queries.** Finding all instances of a component or its parts in
28+
the DOM (e.g., all selected items in an action list) requires knowledge of
29+
internal implementation details.
30+
- **Style overrides.** When consumers do need to customize appearance, CSS
31+
Module hashes are not stable across versions, leaving no reliable selector to
32+
target.
2733

2834
The `data-component` attribute is already used internally across multiple
2935
components (`Button`, `ActionList`, `PageHeader`, `UnderlineNav`, `Avatar`, and
@@ -43,14 +49,31 @@ without notice and coverage is incomplete — many component parts have no
4349

4450
## Decision
4551

46-
Establish two **public, stable data attributes** for identifying components and
47-
their parts in the DOM:
52+
Establish two **public, stable data attributes** that act as reliable
53+
identifiers for Primer components and their parts in the DOM. Think of these as
54+
built-in test IDs — stable, predictable anchors that consumers can use for
55+
testing, tracking, monitoring, and querying without coupling to internal DOM
56+
structure.
4857

4958
- **`data-component`** — identifies the root element of a component or
5059
sub-component.
5160
- **`data-part`** — identifies an inner structural part within a component.
5261

53-
### Naming convention
62+
These attributes are primarily intended for:
63+
64+
1. **Testing** — use them as locators in unit tests, integration tests, and
65+
end-to-end tests instead of brittle class names or DOM paths.
66+
2. **Tracking and monitoring** — query the DOM for component usage metrics,
67+
analytics, or observability.
68+
3. **JavaScript queries** — find specific components or parts
69+
programmatically (e.g.,
70+
`document.querySelectorAll('[data-component="ActionList.Item"]')`).
71+
4. **Simple CSS overrides** — as an added benefit, these selectors also
72+
provide a stable target for style customizations when needed.
73+
74+
### Implementation details
75+
76+
#### Naming convention
5477

5578
All values use PascalCase. The two attributes serve distinct roles:
5679

@@ -59,7 +82,7 @@ data-component="ComponentName" → root element of a component or sub-compon
5982
data-part="PartName" → inner part within a component
6083
```
6184

62-
#### Rules
85+
##### Rules
6386

6487
1. **Root components** get `data-component` with their React component name.
6588

@@ -104,32 +127,46 @@ data-part="PartName" → inner part within a component
104127
</li>
105128
```
106129

107-
### Relationship to CSS Modules and CSS Layers
130+
#### `data-component` and `data-part` on all components and slots
108131

109-
`data-component` and `data-part` complement the existing styling architecture:
132+
All Primer components will receive a `data-component` attribute on their root
133+
element, and all meaningful slots will receive a `data-part` attribute. These
134+
attributes serve as the stable identifiers that consumers can use for targeting,
135+
ensuring full coverage across the library.
110136

111-
- **CSS Modules** provide scoped class names for internal styling. Components
112-
continue to use CSS Module classes for their own styles.
113-
- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer
114-
overrides take precedence over component styles regardless of specificity.
115-
- **`data-component` and `data-part`** provide the stable selectors that
116-
consumers use to target components and their parts within those overrides.
137+
Every component must provide:
138+
139+
- **`data-component`** on the root element of every component and public
140+
sub-component
141+
- **`data-part`** on every internal structural element that a consumer might
142+
reasonably need to target (labels, content wrappers, visual slots, action
143+
slots)
117144

118-
Together, these three mechanisms give consumers a complete override path:
145+
Elements that are purely for layout and have no semantic meaning (spacers,
146+
wrappers that exist only for CSS grid/flex layout) do not require either
147+
attribute.
119148

120-
```css
121-
/* Target a component */
122-
[data-component='ActionList.Item'] {
123-
border-radius: 8px;
124-
}
149+
#### ESLint rule in `eslint-plugin-primer-react`
125150

126-
/* Target a slot within a component */
127-
[data-component='ActionList.Item'] [data-part='Label'] {
128-
font-weight: 600;
129-
}
151+
A new ESLint rule will be added to `eslint-plugin-primer-react` that **warns**
152+
whenever a `data-component` or `data-part` selector is used in consumer code. The warning will
153+
inform consumers of the implications of relying on these selectors — namely that
154+
the surrounding DOM structure, attributes, parents, and children of the targeted
155+
element are not guaranteed to be stable.
156+
157+
Consumers must **explicitly override** the
158+
rule on a per-usage basis to use a `data-component` selector. Overriding the
159+
rule acts as an acknowledgment that the consumer understands the risks and
160+
accepts responsibility for any breakage caused by structural changes to the
161+
component's DOM.
162+
163+
```js
164+
/* eslint-disable primer-react/no-data-component-selector --
165+
Intentionally targeting ActionList.Item for custom border radius.
166+
We accept that the surrounding DOM may change. */
130167
```
131168

132-
### Internal CSS usage
169+
#### Internal CSS usage
133170

134171
Components may use `data-part` selectors in their own CSS Modules for targeting
135172
child parts. This replaces ad-hoc patterns like bare `[data-component='text']`
@@ -142,73 +179,100 @@ with the standardized naming:
142179
}
143180
```
144181

145-
### Coverage requirements
146-
147-
Every component must provide:
148-
149-
- **`data-component`** on the root element of every component and public
150-
sub-component
151-
- **`data-part`** on every internal structural element that a consumer might
152-
reasonably need to target (labels, content wrappers, visual slots, action
153-
slots)
154-
155-
Elements that are purely for layout and have no semantic meaning (spacers,
156-
wrappers that exist only for CSS grid/flex layout) do not require either
157-
attribute.
158-
159-
### Testing requirements
182+
#### Testing requirements
160183

161184
The presence and values of `data-component` and `data-part` attributes must be
162185
covered by tests. This can be achieved through:
163186

164187
- Unit tests that assert the attributes are present on rendered elements
165188
- Snapshot tests that capture the attribute values
166189

167-
Changing a `data-component` or `data-part` value is a **breaking change** and
168-
must follow the standard breaking change process.
190+
#### Documentation on Primer Docs
169191

170-
### Versioning and breaking changes
192+
A dedicated section will be added to Primer Docs explaining what the stable
193+
selectors are intended for and what they are not intended for.
171194

172-
Because `data-component` and `data-part` are **public API**, changes to them
173-
follow [Semantic Versioning](../versioning.md). The table below summarises what
174-
requires which kind of release:
195+
**Intended use cases:**
175196

176-
| Change | semver bump |
177-
| ------------------------------------------------------------------ | ----------- |
178-
| A `data-component` or `data-part` attribute is added to an element | `minor` |
179-
| A `data-component` or `data-part` value is renamed | `major` |
180-
| A `data-component` or `data-part` attribute is removed | `major` |
181-
| An attribute is moved from one element to another | `major` |
182-
| A `data-component` is changed to `data-part` or vice-versa | `major` |
197+
- **Unit tests** — targeting components and slots in test assertions
198+
(e.g., `getByAttribute('data-component', 'ActionList.Item')`)
199+
- **End-to-end testing** — using stable selectors in Playwright or Cypress
200+
locators instead of brittle class names or DOM structure
201+
- **Simple CSS selectors** — directly targeting a component or slot for style
202+
overrides (e.g., `[data-component="Button"] { border-radius: 8px; }`)
183203

184-
**Deprecation path.** Before removing or renaming a value in a major release,
185-
the old value should be deprecated in at least one prior minor release. During
186-
the deprecation window the component must emit a development-mode console
187-
warning (using the existing `warn` / `deprecate` helpers) so consumers have
188-
time to migrate.
204+
**Not intended for:**
189205

190-
The [Migration](#migration) table below captures the full set of renames
191-
planned for the next major release.
206+
- **Complex CSS selector chaining** — sibling selectors (`~`, `+`), child
207+
combinators (`>`), or parent selectors (`:has()`) that depend on DOM structure
208+
- **CSS hacks** — workarounds that rely on the internal layout, nesting, or
209+
ordering of elements within a component
210+
- **Any code that assumes a specific DOM structure** — the DOM tree around a
211+
`data-component` element (its parent, children, siblings, and attributes) is
212+
not part of the public API and may change without notice
213+
214+
The best solution is always to work alongside Primer for your use case. If you
215+
find yourself needing overly complex selectors or targeting `data-component`
216+
attributes for something bigger than their intended use, chances are there is a
217+
better approach. The Primer team is happy to help, guide, and assist with
218+
whatever your use case may be. The design system is always growing — if we are
219+
not covering your use case, we would love to hear from you, or even better,
220+
accept a contribution!
221+
222+
### Relationship to CSS Modules and CSS Layers
223+
224+
While the primary purpose of `data-component` and `data-part` is identification
225+
(testing, tracking, querying), they also serve as stable selectors for CSS
226+
overrides when consumers need to customize appearance.
227+
228+
In that context, they complement the existing styling architecture:
229+
230+
- **CSS Modules** provide scoped class names for internal styling. Components
231+
continue to use CSS Module classes for their own styles.
232+
- **CSS Layers** ([ADR-021](./adr-021-css-layers.md)) ensure that consumer
233+
overrides take precedence over component styles regardless of specificity.
234+
- **`data-component` and `data-part`** provide the stable selectors that
235+
consumers can use to target components and their parts within those overrides.
236+
237+
Example of a simple, supported override:
238+
239+
> **Note:** This is an example for demo purposes only. In practice,
240+
> `ActionList.Item` accepts a `className` prop, which is better suited for this
241+
> type of override. You should always prefer the `className` prop over a stable
242+
> selector when possible.
243+
244+
```css
245+
[data-component='ActionList.Item'] {
246+
border-radius: 8px;
247+
}
248+
```
192249

193250
### Versioning and breaking changes
194251

195-
Because `data-component` and `data-part` are **public API**, changes to them
196-
follow [Semantic Versioning](../versioning.md). The table below summarises what
197-
requires which kind of release:
252+
The **only** guarantee provided for `data-component` and `data-part` attributes is
253+
that they **exist** and are **tied to the most relevant DOM element** for each
254+
component and its slots.
198255

199-
| Change | semver bump |
200-
| ------------------------------------------------------------------ | ----------- |
201-
| A `data-component` or `data-part` attribute is added to an element | `minor` |
202-
| A `data-component` or `data-part` value is renamed | `major` |
203-
| A `data-component` or `data-part` attribute is removed | `major` |
204-
| An attribute is moved from one element to another | `major` |
205-
| A `data-component` is changed to `data-part` or vice-versa | `major` |
256+
The following aspects are **not** part of the public API and may change at any
257+
time without notice or a major semver bump:
206258

207-
**Deprecation path.** Before removing or renaming a value in a major release,
208-
the old value should be deprecated in at least one prior minor release. During
209-
the deprecation window the component must emit a development-mode console
210-
warning (using the existing `warn` / `deprecate` helpers) so consumers have
211-
time to migrate.
259+
- The specific DOM element a `data-component` or `data-part` attribute is
260+
applied to
261+
- The attributes, parent, or children of that element
262+
263+
Because of this, consumers should only rely on the **direct targeting** of
264+
these selectors (e.g., `[data-component="ActionList.Item"]`). Chaining
265+
selectors that depend on DOM structure — such as parent, child, or sibling
266+
selectors — is **not supported** and may break without warning.
267+
268+
| Change | semver bump |
269+
| ------------------------------------------------------------------ | ------------- |
270+
| A `data-component` or `data-part` attribute is added to an element | `minor` |
271+
| A `data-component` or `data-part` value is renamed | `major` |
272+
| A `data-component` or `data-part` attribute is removed | `major` |
273+
| A `data-component` is changed to `data-part` or vice-versa | `major` |
274+
| The DOM element an attribute is applied to changes | `minor` |
275+
| Attributes, parents, or children of the element change | `patch/minor` |
212276

213277
The [Migration](#migration) table below captures the full set of renames
214278
planned for the next major release.
@@ -253,9 +317,13 @@ Components that currently have no attributes on key parts must also be updated.
253317

254318
### Positive
255319

256-
- **Stable selectors for consumers.** Consumers can target any component with
257-
`[data-component="..."]` and any inner part with `[data-part="..."]` — both
258-
are immune to CSS Module hash changes and version upgrades.
320+
- **Stable identifiers for testing.** Consumers can use `data-component` and
321+
`data-part` as reliable locators in unit tests, integration tests, and
322+
end-to-end tests — no more coupling to CSS Module hashes or DOM structure.
323+
- **Enables tracking and monitoring.** Consumers can query the DOM for component
324+
usage metrics and observability without relying on implementation details.
325+
- **Enables JavaScript queries.** Consumers and tests can use
326+
`querySelectorAll('[data-component="ActionList.Item"]')` reliably.
259327
- **Clear separation.** `data-component` answers "which component is this?"
260328
while `data-part` answers "which part of the component is this?" This makes
261329
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.
264332
- **Scoped slot names.** Because `data-part` values are scoped to their parent
265333
`data-component`, names like `Label` or `LeadingVisual` can be reused across
266334
components without ambiguity.
267-
- **Enables JavaScript queries.** Consumers and tests can use
268-
`querySelectorAll('[data-component="ActionList.Item"] [data-part="Label"]')`
269-
reliably.
270-
- **Complements CSS Layers.** Together with ADR-021, this gives consumers a
271-
complete, specificity-safe override mechanism.
335+
- **Supports CSS overrides.** Consumers who need to
336+
customize styles have stable selectors to target, complementing CSS Layers
337+
(ADR-021) for a complete override path.
272338

273339
### Negative
274340

275341
- **Breaking change for existing consumers.** Anyone currently relying on the
276-
undocumented `data-component` values (e.g., in CSS overrides or
277-
`querySelector` calls) will need to update when values are renamed. This must
278-
be coordinated in a major release.
342+
undocumented `data-component` values (e.g., in tests, tracking code, CSS
343+
overrides, or `querySelector` calls) will need to update when values are
344+
renamed. This must be coordinated in a major release.
279345

280346
## Alternatives
281347

@@ -302,7 +368,7 @@ applicable to our architecture.
302368

303369
Continue using `data-component` informally without guaranteeing stability.
304370

305-
**Why not chosen:** Consumers are already depending on these attributes for
306-
overrides (as seen in SelectPanel story CSS). Without a stability guarantee,
307-
any refactor can silently break consumer overrides. Formalizing the API
308-
acknowledges the reality and provides a proper contract.
371+
**Why not chosen:** Consumers are already depending on these attributes in
372+
tests, tracking code, and CSS overrides. Without a stability guarantee, any
373+
refactor can silently break consumer code. Formalizing the API acknowledges the
374+
reality and provides a proper contract.

0 commit comments

Comments
 (0)