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
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
2834The ` data-component ` attribute is already used internally across multiple
2935components (` 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
5578All 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
5982data-part="PartName" → inner part within a component
6083```
6184
62- #### Rules
85+ ##### Rules
6386
64871 . ** 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
134171Components may use ` data-part ` selectors in their own CSS Modules for targeting
135172child 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
161184The presence and values of ` data-component ` and ` data-part ` attributes must be
162185covered 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
213277The [ Migration] ( #migration ) table below captures the full set of renames
214278planned 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
303369Continue 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