Skip to content

ADR-023: Public data-component API for targeting component parts#7576

Open
lukasoppermann wants to merge 5 commits intomainfrom
young-orbit
Open

ADR-023: Public data-component API for targeting component parts#7576
lukasoppermann wants to merge 5 commits intomainfrom
young-orbit

Conversation

@lukasoppermann
Copy link
Contributor

Adds ADR-023 proposing data-component as a public, stable API for identifying component parts in the DOM. This gives consumers reliable selectors to target internal parts of components for style overrides and JS queries, independent of CSS Module hash changes.

Key decisions:

  • Dot notation mirroring the React API (ActionList.Item, Button.LeadingVisual)
  • State/modifier attrs (data-variant, data-size) remain separate
  • Includes migration table for existing inconsistent values
  • Complements CSS Layers (ADR-021) for a complete override mechanism

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>
@lukasoppermann lukasoppermann requested a review from a team as a code owner February 20, 2026 13:46
@changeset-bot
Copy link

changeset-bot bot commented Feb 20, 2026

⚠️ No Changeset found

Latest commit: fdcd038

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces ADR-023, which formalizes data-component as a public, stable API for identifying component parts in the DOM. The ADR addresses the current problem where CSS Modules generate unstable hashed class names, making it impossible for consumers to reliably target internal component parts for style overrides or JavaScript queries. By establishing a consistent naming convention using dot notation that mirrors the React component API (e.g., ActionList.Item, Button.LeadingVisual), this ADR provides consumers with a reliable mechanism to target component parts.

Changes:

  • Proposes standardized dot notation naming convention (ComponentName.PartName) for data-component attributes
  • Documents migration path for existing inconsistent data-component values across multiple components
  • Establishes testing requirements and coverage guidelines for the data-component API

@lukasoppermann lukasoppermann added the skip changeset This change does not need a changelog label Feb 20, 2026
@github-actions github-actions bot requested a deployment to storybook-preview-7576 February 20, 2026 13:56 Abandoned
@primer
Copy link
Contributor

primer bot commented Feb 20, 2026

🤖 Formatting issues have been automatically fixed and committed to this PR.

Copy link
Member

@siddharthkp siddharthkp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally really like the idea (i think I introduced it to the codebase), so I approve!

target internal parts of a component for style overrides using class selectors.

I know you did not say anything against this but I think this part is worth highlighting more boldly in the ADR because the preferred/superior method is still to expose smaller parts and let people add their own classes to them.

<ActionList.Item className={classes.myCustomList} />
  <ActionList.Description>...</ActionList.Description>
</ActionList.Item>
.myCustomList {} 
.myCustomList [data-component="ActionList.Description"] {}

but it's better to do this:

<ActionList.Item className={classes.myCustomList} />
  <ActionList.Description className={classes.myCustomListDescription}..</ActionList.Description>
</ActionList.Item>
.myCustomList {}
.myCustomListDescription {}

Maybe a better example to showcase in the ADR would be a component where we don't let you compose

<SelectPanel items={...} className={classes.myCustomPanel />
.myCustomPanel [data-component="ActionList.Description"] {}

| -------------------- | ------------------------------------------------ |
| `ComponentName.Part` | `ActionList.Description`, `ActionList.Selection` |
| `PREFIX_Part` | `PH_LeadingAction`, `PH_Title`, `PH_Navigation` |
| `camelCase` | `buttonContent`, `leadingVisual`, `text` |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ouch, i think i know why this is so.

ActionList.Description is a component that is part of the public API, so we expose that exactly.

But Button's leadingVisual is not part of the API so it ended up naming it after the className or internal variable name which is camelCase. I don't know if we need to differentiate them based on the nuance of public vs private, i don't think anyone else would care about it.

PH_LeadingAction, similar reason, but underscore is random, call it PageHeader.LeadingAction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think this is not needed.

@github-actions github-actions bot requested a deployment to storybook-preview-7576 February 20, 2026 14:05 Abandoned
@lukasoppermann
Copy link
Contributor Author

lukasoppermann commented Feb 20, 2026

I personally really like the idea (i think I introduced it to the codebase), so I approve!

target internal parts of a component for style overrides using class selectors.

I know you did not say anything against this but I think this part is worth highlighting more boldly in the ADR because

Agree. I think this is a quick solution, that may still be helpful if you want to do small styling changes for all actionList.items or something.

But creating more composable subcomponents is definitely an additional need.

@siddharthkp feel free to adjust with a better example if you want.

Copy link
Member

@TylerJDev TylerJDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving! Left some comments.

<li data-component="ActionList.Item"></li>
```

3. **Internal structural parts** (DOM elements that are not exposed as a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we determine which internal parts are meaningful enough to have a data-component attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, any ideas?

I think one aspect could be that they are functional, so not simple structure or a11y elements.

Do you think we will run into issues here or should we maybe just try it and see if it just works out? We could try to compile a list of what might make a meaningful element if you think this is helpful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any harm in over-doing it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any harm in over-doing it?

My take as well! let's do all the things

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good change! Is this something consumers are currently asking for, or are we anticipating future needs and implementing it in advance?

Copy link
Contributor

@hectahertz hectahertz Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1000% to @TylerJDev comment, I'd please like to see real problems and use cases for this that justify this.

See this https://github.com/github/primer/discussions/6393 for why I'm suggesting changes. I think this is unnecessary and could lead to a very fragile system.

Copy link
Member

@siddharthkp siddharthkp Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From slack, @hectahertz
Why is this a problem? do we want people changing internals outside of the api? Free reign?

I think this is slightly tangential to your post. The root of the problem is some components not being composable at all. For example, SelectPanel and Autocomplete are black boxes that take title, items, etc. but gives you little control over customising the styles of internal components

@hectahertz: What's the plan for us restyling those classes? are we going to have to make major releases anytime we change the styling of one of them or the dom structure as they will be breaking changes?

This is a tricky spot we always find ourselves in. If we don't give folks the ability to customise, the only option is to fork/eject. If we do give some customisations, we are now on the hook to support backward compatibility with those customisations as well. 😢

For me, a balance does not exist in the long term, because it's not uncommon for us to need to change the internal implementation details for accessibility or other fixes. I'd love to hear ideas on how we can solve for this

Copy link
Contributor Author

@lukasoppermann lukasoppermann Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am currently doing interviews with folks and have hear this as an issue a few times.

I don't have a list of the specific things, but I think especially copilot like copilot chat, ran into a lot of issues where they wanted to change small things but couldn't easily and thus rebuilt the component.

@hectahertz are you suggesting that adding data-attr will have a huge performance impact or are you saying teams can kill performance by messing with the components? Can you elaborate on why this would make the system fragile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So enterprise has some cases where they are trying to style components different. Currently they are wrapping the react component into another react component to add styles. However, it is hard to target specific parts. The span is actually the trailing action part they want to style.

.chipInput {
  border-radius: var(--borderRadius-full) !important;

  & input:placeholder-shown + span {
    display: none;
  }
}

.chipInput span:last-child {
  color: var(--control-fgColor-disabled) !important;

  &:hover {
    color: var(--fgColor-muted) !important;
    cursor: pointer;
  }
}

Copy link
Member

@siddharthkp siddharthkp Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukasoppermann sorry, what component is that? Trying to see if there's a better way that already exists for this specific case

For example, with TextInput, the ideal solution that @hectahertz suggested already works:

<TextInput trailingVisual={() => <CalendarIcon className="custom-selector" />} />

and then you can target it

. & input:placeholder-shown + .custom-selector {}

I feel this is better than

. & input:placeholder-shown + [data-component=TextInput.TrailingVisual] {}

because it also let's you do other things like

<TextInput trailingVisual={() => <CalendarIcon data-testid="id" />} />

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siddharthkp
They are trying to replicate the new filter UI style.

For your example, yes, this of course is nice.
I have two issues with that:

  1. We don't have this for all our components, and it seems like this will not be a quick fix.
  2. This means that if I want to just change the text color for something, or icon color, I always need to add nested components for everything, this may be a bit of on overkill for small changes.

However, I think 2 is a matter of opinion and I am happy to shift there, but I see 1 as a problem.

Do you see this differently?

Another example I remember is a case where folks needed to change the color of the icon in the banner, which I think is not easily possible.

Copy link
Member

@siddharthkp siddharthkp Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see this differently?

Nope, same page as you

  1. We don't have this for all our components, and it seems like this will not be a quick fix.

Yes, absolutely. I was trying to point the ideal scenario of how it should be.

  1. This means that if I want to just change the text color for something, or icon color, I always need to add nested components for everything, this may be a bit of on overkill for small changes.

Maybe but still very little additional work, so for customisation that goes off the system, I don't feel too bad about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example use cases I learned yesterday. Dustin needed to adjust the Dialog position as he was using it to build the command palette. He only made it work with a css hack, as the class is not forwarded to the needed element.

He said that having a way to at least temporary move on (and possibly report a bug / feature request to primer) would have helped him a lot.


### Negative

- **Breaking change for existing consumers.** Anyone currently relying on the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we have data on how many data-component attributes are used in CSS/DOM queries today. Either way, I don't anticipate a difficult migration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

72 files in github/github-ui, not all of them are primer components, but I'm assuming most of them are

Copy link

@joshfarrant joshfarrant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great! :shipit:

Stable, predictable component identifiers in the DOM could also be useful for AI agents too. An agent inspecting rendered output could use data-component values to understand the component hierarchy without needing access to source code, which is a nice added benefit 🦾


### Migration

Existing `data-component` values must be migrated to the new convention. This

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're formalising a data attribute for component identification, do you think there's any benefit in namespacing the data attribute? E.g.,

<div data-primer-component="Button.Content">

It seems unlikely that there would ever be a collision with other libraries/projects as I can't name any which use data-component, but I wonder whether you'd considered namespacing it as part of the formalisation of the attribute?

It could also help ease the transition too, as introducing a new attribute (data-primer-component) rather than renaming values on the existing data-component would allow both to coexist during a deprecation period. Consumers could migrate their selectors incrementally rather than having all existing data-component references break at once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting idea. I'd be open to it. Wonder what other folks think @siddharthkp @TylerJDev ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, i don't feel strongly about it. I like the simplicity of data-component, I'd rather keep it.

Consumers could migrate their selectors incrementally rather than having all existing data-component references break at once.

Depends on how many we have. It's easy to target both old and new values in a css selector, so we can migrate without a breaking change.

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:
Copy link
Contributor

@hectahertz hectahertz Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1000% to @TylerJDev comment, I'd please like to see real problems and use cases for this that justify this.

See this https://github.com/github/primer/discussions/6393 for why I'm suggesting changes. I think this is unnecessary and could lead to a very fragile system.

**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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we look into why they are doing that? I think we should categorize and solve those use cases properly instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a two step process:

  1. Teams adjust / experiment using data attribute selectors
  2. Primer team upstreams proven customizations to the system

The problem is that we can't deliver at feature team speed and that the teams need ways to customize components to explore ideas e.g. in flagged ships.

If they always have to rebuild everything, it is unlikely they adjust it to use primer and create a PR once they have proven their idea is needed, because for them it brings little benefit compared to focusing on a new feature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of this , cuts down on teams having to wait for Primer to deliver on small customization/changes. We can then upstream and cleanup without teams having to be blocked for a week waiting on a Primer release.

@lukasoppermann
Copy link
Contributor Author

lukasoppermann commented Feb 24, 2026

Some systems, like shd/cdn use data-slot e.g. here. This allows for a combination of data-component on the actual component e.g. ActionMenu.Button and data-slot on the sub-elements like like TrailingAction (this actually uses a data-component at the moment).

I think this is nice, as you can easily nest it like [data-component="Button"] [data-slot="icon"], but I am not sure if it is a great idea.

What are your opinions? @hectahertz @siddharthkp @joshfarrant @TylerJDev @francinelucca?

Copy link
Member

@francinelucca francinelucca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

**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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of this , cuts down on teams having to wait for Primer to deliver on small customization/changes. We can then upstream and cleanup without teams having to be blocked for a week waiting on a Primer release.

@siddharthkp siddharthkp added the integration-tests: skipped manually Changes in this PR do not require an integration test label Feb 25, 2026
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>
@lukasoppermann
Copy link
Contributor Author

I now updated the ADR to include data-slot.

@github-actions github-actions bot requested a deployment to storybook-preview-7576 February 26, 2026 14:22 Abandoned
@siddharthkp
Copy link
Member

siddharthkp commented Feb 27, 2026

Some systems, like shd/cdn use data-slot e.g. here. This allows for a combination of data-component on the actual component e.g. ActionMenu.Button and data-slot on the sub-elements like like TrailingAction (this actually uses a data-component at the moment).

Honestly, not a fan of having both. Whether something is a data-component because it composes another primer component or is a data-slot because its local to the component (for now) is an implementation detail that should not matter to a consumer.

Pick one or the other for simplicity. data-slot makes sense when you don't have data-component at all because you can target it directly.

@lukasoppermann
Copy link
Contributor Author

Some systems, like shd/cdn use data-slot e.g. here. This allows for a combination of data-component on the actual component e.g. ActionMenu.Button and data-slot on the sub-elements like like TrailingAction (this actually uses a data-component at the moment).

Honestly, not a fan of having both. Whether something is a data-component because it composes another primer component or is a data-slot because its local to the component (for now) is an implementation detail that should not matter to a consumer.

Pick one or the other for simplicity. data-slot makes sense when you don't have data-component at all because you can target it directly.

@siddharthkp if we just add data-slot for inner parts, users could add their own class, e.g. .button-blue and use the slot: .button-blue [data-slot="icon"]. So I think this would be enough and we could ignore data-component on the components for now.

When we have subcomponents, like ActionList.Item are they always placed there by the user so that they could add a class?

@siddharthkp
Copy link
Member

When we have subcomponents, like ActionList.Item are they always placed there by the user so that they could add a class?

Correct.

Unless, the ActionList is rendered by SelectPanel, then you don't have access to it, because the SelectPanel does not let you compose :(

@siddharthkp if we just add data-slot for inner parts, users could add their own class, e.g. .button-blue and use the slot: .button-blue [data-slot="icon"].

Correct.

So I think this would be enough and we could ignore data-component on the components for now.

If it's all the same, I'd like to keep the name data-component because we already have it and slot can be an implementation detail, not all sub components are "slots"

@github-actions github-actions bot requested a deployment to storybook-preview-7576 March 3, 2026 08:27 Abandoned
@github-actions github-actions bot requested a deployment to storybook-preview-7576 March 3, 2026 08:35 Abandoned
@github-actions github-actions bot requested a deployment to storybook-preview-7576 March 3, 2026 14:19 Abandoned
@github-actions github-actions bot requested a deployment to storybook-preview-7576 March 3, 2026 14:22 Abandoned
@hectahertz
Copy link
Contributor

hectahertz commented Mar 3, 2026

Approving not to block this if you want to merge it, even though I still have the same concerns. Please consider my previous comments and how you'll maintain semver if this gets implemented.

This will be hard to undo if consumers start using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: skipped manually Changes in this PR do not require an integration test skip changeset This change does not need a changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants