From 618342f01a486f0ae802ea7fefb44f4569bacb43 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 26 Feb 2026 14:29:38 +0100 Subject: [PATCH 1/7] feat(SegmentedControl): add CSS custom property tokens for external overrides Define component-level CSS custom properties on the root .SegmentedControl element so they can be overridden from parent elements or via inline styles: - --segmented-control-bgColor - --segmented-control-bgColor-hover - --segmented-control-bgColor-active - --segmented-control-borderColor - --segmented-control-borderRadius - --segmented-control-fgColor-icon - --segmented-control-fgColor-disabled - --segmented-control-fontWeight - --segmented-control-selected-bgColor - --segmented-control-selected-borderColor - --segmented-control-selected-fontWeight - --segmented-control-button-inner-padding - --segmented-control-button-bg-inset Also adds a CustomCSSTokens feature story demonstrating how to override tokens from a parent element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SegmentedControl.features.stories.tsx | 26 +++++++ .../SegmentedControl.module.css | 70 +++++++++++++------ 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx index 363ed43cfd8..43221a20ae5 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx @@ -1,3 +1,4 @@ +import type React from 'react' import {useState} from 'react' import type {Meta} from '@storybook/react-vite' import {SegmentedControl} from '.' @@ -154,3 +155,28 @@ export const AssociatedWithALabelAndCaption = () => ( ) AssociatedWithALabelAndCaption.storyName = '[Example] Associated with a label and caption' + +export const CustomCSSTokens = () => ( +
+ + + Preview + + + Raw + + + Blame + + +
+) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 22b80ae8f33..59fb69953fd 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,4 +1,21 @@ .SegmentedControl { + /* Component tokens – override these custom properties to customize the control */ + --segmented-control-bgColor: var(--controlTrack-bgColor-rest); + --segmented-control-bgColor-hover: var(--controlTrack-bgColor-hover); + --segmented-control-bgColor-active: var(--controlTrack-bgColor-active); + --segmented-control-borderColor: var(--controlTrack-borderColor-rest, transparent); + --segmented-control-borderRadius: var(--borderRadius-medium); + --segmented-control-fgColor-icon: var(--fgColor-muted); + --segmented-control-fgColor-disabled: var(--fgColor-disabled); + --segmented-control-fontWeight: var(--base-text-weight-normal); + --segmented-control-selected-bgColor: var(--controlKnob-bgColor-rest); + --segmented-control-selected-borderColor: var(--controlKnob-borderColor-rest); + --segmented-control-selected-fontWeight: var(--base-text-weight-semibold); + + /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ + --segmented-control-button-inner-padding: 12px; + --segmented-control-button-bg-inset: 4px; + /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ --segmented-control-icon-width: 32px; @@ -9,9 +26,12 @@ padding: 0; margin: 0; font-size: var(--text-body-size-medium); - background-color: var(--controlTrack-bgColor-rest); - border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent); - border-radius: var(--borderRadius-medium); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--segmented-control-bgColor); + /* stylelint-disable-next-line primer/colors */ + border: var(--borderWidth-thin) solid var(--segmented-control-borderColor); + /* stylelint-disable-next-line primer/borders */ + border-radius: var(--segmented-control-borderRadius); /* Responsive full-width */ &[data-full-width='true'] { @@ -209,29 +229,26 @@ } .Button { - /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ - --segmented-control-button-inner-padding: 12px; - --segmented-control-button-bg-inset: 4px; - --segmented-control-outer-radius: var(--borderRadius-medium); - width: 100%; height: 100%; /* stylelint-disable-next-line primer/spacing */ padding: var(--segmented-control-button-bg-inset); font-family: inherit; font-size: inherit; - font-weight: var(--base-text-weight-normal); + font-weight: var(--base-text-weight-light); color: currentColor; cursor: pointer; background-color: transparent; border-color: transparent; border-width: 0; /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-outer-radius); + border-radius: var(--segmented-control-borderRadius); & svg { - fill: var(--fgColor-muted); - color: var(--fgColor-muted); + /* stylelint-disable-next-line primer/colors */ + fill: var(--segmented-control-fgColor-icon); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmented-control-fgColor-icon); } /* fallback :focus state */ @@ -261,12 +278,15 @@ &[aria-disabled='true']:not([aria-current='true']) { cursor: not-allowed; - color: var(--fgColor-disabled); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmented-control-fgColor-disabled); background-color: transparent; & svg { - fill: var(--fgColor-disabled); - color: var(--fgColor-disabled); + /* stylelint-disable-next-line primer/colors */ + fill: var(--segmented-control-fgColor-disabled); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmented-control-fgColor-disabled); } } @@ -303,34 +323,38 @@ https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn */ /* stylelint-disable-next-line primer/borders */ - border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2); + border-radius: calc(var(--segmented-control-borderRadius) - var(--segmented-control-button-bg-inset) / 2); align-items: center; justify-content: center; } .Button[aria-current='true'] { padding: 0; - font-weight: var(--base-text-weight-semibold); + font-weight: var(--base-text-weight-light); .Content { /* stylelint-disable-next-line primer/spacing */ padding-right: var(--segmented-control-button-inner-padding); /* stylelint-disable-next-line primer/spacing */ padding-left: var(--segmented-control-button-inner-padding); - background-color: var(--controlKnob-bgColor-rest); - border-color: var(--controlKnob-borderColor-rest); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--segmented-control-selected-bgColor); + /* stylelint-disable-next-line primer/colors */ + border-color: var(--segmented-control-selected-borderColor); /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-outer-radius); + border-radius: var(--segmented-control-borderRadius); } } .Button:not([aria-current='true'], [aria-disabled='true']) { &:hover .Content { - background-color: var(--controlTrack-bgColor-hover); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--segmented-control-bgColor-hover); } &:active .Content { - background-color: var(--controlTrack-bgColor-active); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--segmented-control-bgColor-active); } } @@ -338,7 +362,7 @@ display: block; height: 0; overflow: hidden; - font-weight: var(--base-text-weight-semibold); + font-weight: var(--base-text-weight-light); pointer-events: none; visibility: hidden; content: attr(data-text); From 6e2beb6e892e4aa4c094c44010493a2914cd74ff Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 26 Feb 2026 15:22:30 +0100 Subject: [PATCH 2/7] fix(SegmentedControl): use var() fallback pattern for overridable CSS tokens CSS custom properties defined directly on an element always win over inherited values from parent elements. Switch to the var(--token, fallback) pattern so that tokens set on a parent element properly cascade down to the SegmentedControl. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SegmentedControl.module.css | 99 +++++++++++-------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 59fb69953fd..47c9fe76388 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,20 +1,21 @@ .SegmentedControl { - /* Component tokens – override these custom properties to customize the control */ - --segmented-control-bgColor: var(--controlTrack-bgColor-rest); - --segmented-control-bgColor-hover: var(--controlTrack-bgColor-hover); - --segmented-control-bgColor-active: var(--controlTrack-bgColor-active); - --segmented-control-borderColor: var(--controlTrack-borderColor-rest, transparent); - --segmented-control-borderRadius: var(--borderRadius-medium); - --segmented-control-fgColor-icon: var(--fgColor-muted); - --segmented-control-fgColor-disabled: var(--fgColor-disabled); - --segmented-control-fontWeight: var(--base-text-weight-normal); - --segmented-control-selected-bgColor: var(--controlKnob-bgColor-rest); - --segmented-control-selected-borderColor: var(--controlKnob-borderColor-rest); - --segmented-control-selected-fontWeight: var(--base-text-weight-semibold); - - /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ - --segmented-control-button-inner-padding: 12px; - --segmented-control-button-bg-inset: 4px; + /* + * Component tokens – override these custom properties from a parent element to customize the control: + * + * --segmented-control-bgColor + * --segmented-control-bgColor-hover + * --segmented-control-bgColor-active + * --segmented-control-borderColor + * --segmented-control-borderRadius + * --segmented-control-fgColor-icon + * --segmented-control-fgColor-disabled + * --segmented-control-fontWeight + * --segmented-control-selected-bgColor + * --segmented-control-selected-borderColor + * --segmented-control-selected-fontWeight + * --segmented-control-button-inner-padding + * --segmented-control-button-bg-inset + */ /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ --segmented-control-icon-width: 32px; @@ -27,11 +28,13 @@ margin: 0; font-size: var(--text-body-size-medium); /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor); - /* stylelint-disable-next-line primer/colors */ - border: var(--borderWidth-thin) solid var(--segmented-control-borderColor); + background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); + /* stylelint-disable primer/colors */ + border: var(--borderWidth-thin) solid + var(--segmented-control-borderColor, var(--controlTrack-borderColor-rest, transparent)); + /* stylelint-enable primer/colors */ /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius); + border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); /* Responsive full-width */ &[data-full-width='true'] { @@ -232,23 +235,23 @@ width: 100%; height: 100%; /* stylelint-disable-next-line primer/spacing */ - padding: var(--segmented-control-button-bg-inset); + padding: var(--segmented-control-button-bg-inset, 4px); font-family: inherit; font-size: inherit; - font-weight: var(--base-text-weight-light); + font-weight: var(--segmented-control-fontWeight, var(--base-text-weight-normal)); color: currentColor; cursor: pointer; background-color: transparent; border-color: transparent; border-width: 0; /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius); + border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); & svg { /* stylelint-disable-next-line primer/colors */ - fill: var(--segmented-control-fgColor-icon); + fill: var(--segmented-control-fgColor-icon, var(--fgColor-muted)); /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-icon); + color: var(--segmented-control-fgColor-icon, var(--fgColor-muted)); } /* fallback :focus state */ @@ -279,14 +282,14 @@ &[aria-disabled='true']:not([aria-current='true']) { cursor: not-allowed; /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-disabled); + color: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); background-color: transparent; & svg { /* stylelint-disable-next-line primer/colors */ - fill: var(--segmented-control-fgColor-disabled); + fill: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-disabled); + color: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); } } @@ -310,10 +313,16 @@ .Content { display: flex; height: 100%; - /* stylelint-disable-next-line primer/spacing */ - padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); - /* stylelint-disable-next-line primer/spacing */ - padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); + /* stylelint-disable primer/spacing */ + padding-right: calc( + var(--segmented-control-button-inner-padding, var(--base-size-12)) - + var(--segmented-control-button-bg-inset, var(--base-size-4)) + ); + padding-left: calc( + var(--segmented-control-button-inner-padding, var(--base-size-12)) - + var(--segmented-control-button-bg-inset, var(--base-size-4)) + ); + /* stylelint-enable primer/spacing */ border-color: transparent; border-style: solid; border-width: var(--borderWidth-thin); @@ -322,39 +331,43 @@ innerRadius = outerRadius - distance/2 https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn */ - /* stylelint-disable-next-line primer/borders */ - border-radius: calc(var(--segmented-control-borderRadius) - var(--segmented-control-button-bg-inset) / 2); + /* stylelint-disable primer/borders */ + border-radius: calc( + var(--segmented-control-borderRadius, var(--borderRadius-medium)) - var(--segmented-control-button-bg-inset, 4px) / + 2 + ); + /* stylelint-enable primer/borders */ align-items: center; justify-content: center; } .Button[aria-current='true'] { padding: 0; - font-weight: var(--base-text-weight-light); + font-weight: var(--segmented-control-selected-fontWeight, var(--base-text-weight-semibold)); .Content { /* stylelint-disable-next-line primer/spacing */ - padding-right: var(--segmented-control-button-inner-padding); + padding-right: var(--segmented-control-button-inner-padding, 12px); /* stylelint-disable-next-line primer/spacing */ - padding-left: var(--segmented-control-button-inner-padding); + padding-left: var(--segmented-control-button-inner-padding, 12px); /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-selected-bgColor); + background-color: var(--segmented-control-selected-bgColor, var(--controlKnob-bgColor-rest)); /* stylelint-disable-next-line primer/colors */ - border-color: var(--segmented-control-selected-borderColor); + border-color: var(--segmented-control-selected-borderColor, var(--controlKnob-borderColor-rest)); /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius); + border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); } } .Button:not([aria-current='true'], [aria-disabled='true']) { &:hover .Content { /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor-hover); + background-color: var(--segmented-control-bgColor-hover, var(--controlTrack-bgColor-hover)); } &:active .Content { /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor-active); + background-color: var(--segmented-control-bgColor-active, var(--controlTrack-bgColor-active)); } } @@ -362,7 +375,7 @@ display: block; height: 0; overflow: hidden; - font-weight: var(--base-text-weight-light); + font-weight: var(--segmented-control-selected-fontWeight, var(--base-text-weight-semibold)); pointer-events: none; visibility: hidden; content: attr(data-text); From f125d26df5d2291a643a3c42094bd8c655d1c370 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 26 Feb 2026 15:45:22 +0100 Subject: [PATCH 3/7] feat(SegmentedControl): add selected text/icon color token Add --segmented-control-selected-fgColor token to control the text and icon color of the selected segment. Update the CustomCSSTokens story to set it to white (--fgColor-onEmphasis) for contrast on the accent background. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SegmentedControl.features.stories.tsx | 1 + .../src/SegmentedControl/SegmentedControl.module.css | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx index 43221a20ae5..e086c3e1454 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx @@ -164,6 +164,7 @@ export const CustomCSSTokens = () => ( '--segmented-control-borderColor': 'var(--borderColor-accent-emphasis)', '--segmented-control-selected-bgColor': 'var(--bgColor-accent-emphasis)', '--segmented-control-selected-borderColor': 'var(--borderColor-accent-emphasis)', + '--segmented-control-selected-fgColor': 'var(--fgColor-onEmphasis)', } as React.CSSProperties } > diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 47c9fe76388..72fa983675f 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -12,6 +12,7 @@ * --segmented-control-fontWeight * --segmented-control-selected-bgColor * --segmented-control-selected-borderColor + * --segmented-control-selected-fgColor * --segmented-control-selected-fontWeight * --segmented-control-button-inner-padding * --segmented-control-button-bg-inset @@ -344,6 +345,15 @@ .Button[aria-current='true'] { padding: 0; font-weight: var(--segmented-control-selected-fontWeight, var(--base-text-weight-semibold)); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmented-control-selected-fgColor, inherit); + + & svg { + /* stylelint-disable-next-line primer/colors */ + fill: var(--segmented-control-selected-fgColor, inherit); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmented-control-selected-fgColor, inherit); + } .Content { /* stylelint-disable-next-line primer/spacing */ From b3d9171a44a52ef7c2d81b8b2e7793788b9a4f18 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Thu, 26 Feb 2026 16:47:36 +0100 Subject: [PATCH 4/7] docs: add ADR-023 for component tokens as CSS custom properties Establishes the pattern for exposing overridable CSS custom properties as component tokens. Covers naming conventions, what to tokenize vs not, the var() fallback pattern for inheritance, and consumer usage examples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-component-tokens.md | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 contributor-docs/adrs/adr-023-component-tokens.md diff --git a/contributor-docs/adrs/adr-023-component-tokens.md b/contributor-docs/adrs/adr-023-component-tokens.md new file mode 100644 index 00000000000..b251f666e74 --- /dev/null +++ b/contributor-docs/adrs/adr-023-component-tokens.md @@ -0,0 +1,198 @@ +# [ADR] Component tokens as CSS custom properties + +📆 Date: 2026-02-26 + +## Status + +| Stage | State | +| -------------- | -------------- | +| Status | Proposed ❓ | +| Implementation | Not planned ⛔ | + +## Context + +Primer React components use design tokens from `@primer/primitives` for colors, spacing, typography, and borders. These tokens enforce consistency across the design system but make it difficult for consumers to customize the appearance of individual component instances without resorting to fragile CSS overrides targeting internal class names. + +A common need is to restyle a component for a specific context — for example, rendering a `SegmentedControl` with accent colors to indicate a primary action area, or adjusting border radius to match a custom layout. Today, the only options are: + +1. **Override internal CSS module class names** — brittle, breaks on refactors, and not part of the public API. +2. **Use inline styles on the component** — limited to properties exposed on the root element and cannot target internal elements like the selected state or hover states. +3. **Wrap in a themed provider** — too broad, affects all components in the subtree. + +Component tokens solve this by exposing a stable set of CSS custom properties that consumers can set on a parent element or the component itself to customize its appearance. + +## Decision + +Every component should expose a set of **component tokens** as CSS custom properties. These tokens are the public styling API of the component and follow a consistent pattern across all components. + +### Naming convention + +Component tokens use the following naming pattern: + +``` +--{component-name}-{property} +--{component-name}-{variant}-{property} +``` + +Examples from `SegmentedControl`: + +| Token | Purpose | +| ----------------------------------------- | ------------------------------------ | +| `--segmented-control-bgColor` | Track background color | +| `--segmented-control-bgColor-hover` | Track background on hover | +| `--segmented-control-borderColor` | Outer border color | +| `--segmented-control-borderRadius` | Outer border radius | +| `--segmented-control-fgColor-icon` | Icon color (default state) | +| `--segmented-control-fontWeight` | Text weight (default state) | +| `--segmented-control-selected-bgColor` | Selected segment background | +| `--segmented-control-selected-fgColor` | Selected segment text and icon color | +| `--segmented-control-selected-fontWeight` | Selected segment text weight | + +### What to tokenize + +Expose tokens for visual properties that a consumer may reasonably need to customize: + +- **Colors** — background, foreground (text), border, and icon colors for each distinct state (rest, hover, active, selected, disabled). +- **Border radius** — the outer shape of the component. +- **Font weight** — when the component uses non-standard weights (e.g., semibold for selected state). +- **Spacing** — internal padding values that affect the component's visual density, when they are not derived from a standard size primitive. + +### What NOT to tokenize + +Do not expose tokens for: + +- **Layout properties** — `display`, `width`, `height`, `flex`, `position`. These are structural and changing them would break the component. +- **Focus styles** — outline color, offset, and box-shadow for focus states must remain consistent for accessibility. +- **Font size** — controlled by the `size` prop and design system typography scale. +- **Font family** — inherited from the page and should not vary per-component. +- **Cursor** — semantic (e.g., `pointer` for clickable, `not-allowed` for disabled) and should not be overridden. +- **Internal structural values** — z-index, pseudo-element positioning, transforms used for hit areas. + +### Implementation pattern: `var()` with fallback + +**Do not** define component tokens as custom properties on the component element itself. Setting a property on the element always wins over inherited values, which prevents consumers from overriding tokens from a parent element. + +❌ **Wrong — blocks inheritance:** + +```css +.SegmentedControl { + --segmented-control-bgColor: var(--controlTrack-bgColor-rest); + background-color: var(--segmented-control-bgColor); +} +``` + +With this pattern, setting `--segmented-control-bgColor` on a parent `
` has no effect because the component redefines it on its own element. + +✅ **Correct — use `var()` with a fallback value:** + +```css +.SegmentedControl { + background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); +} +``` + +The token is never defined by the component. It only references it with a fallback. If a consumer sets `--segmented-control-bgColor` on any ancestor element, that value is inherited and used. If not, the fallback kicks in. + +For hardcoded fallback values (not from primitives), use the literal value directly: + +```css +.Button { + padding: var(--segmented-control-button-bg-inset, 4px); +} +``` + +### Documentation in CSS + +List all available component tokens in a comment block at the top of the component's root selector: + +```css +.SegmentedControl { + /* + * Component tokens – override these custom properties from a parent + * element to customize the control: + * + * --segmented-control-bgColor + * --segmented-control-bgColor-hover + * --segmented-control-bgColor-active + * --segmented-control-borderColor + * --segmented-control-borderRadius + * --segmented-control-selected-bgColor + * --segmented-control-selected-borderColor + * --segmented-control-selected-fgColor + * --segmented-control-selected-fontWeight + */ + + background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); + /* ... */ +} +``` + +### Consumer usage + +Consumers override tokens by setting them on a parent element or via inline styles: + +```tsx +
+ + Preview + Raw + +
+``` + +Consumers should prefer using Primer primitive values (e.g., `var(--bgColor-accent-emphasis)`) over hardcoded colors to maintain theme compatibility. + +## Consequences + +### Positive + +- **Stable public API** — consumers can customize components without depending on internal class names. +- **Inheritance-friendly** — tokens set on a parent cascade down, enabling contextual theming. +- **Backward compatible** — existing components continue to work unchanged; tokens are purely additive. +- **Self-documenting** — the comment block in each component's CSS file serves as the token reference. +- **Theme-safe** — fallback values ensure the component always looks correct even if no tokens are set. + +### Negative + +- **Repeated fallback values** — the same fallback (e.g., `var(--controlTrack-bgColor-rest)`) may appear in multiple places within a component's CSS, since tokens are not defined centrally on the element. +- **Stylelint noise** — component tokens trigger `primer/colors` and `primer/borders` lint rules because they are not recognized Primer primitives. Each usage requires a `stylelint-disable` comment. +- **API surface** — once a token is documented and adopted, renaming or removing it is a breaking change per our [versioning policy](../versioning.md#a-component-changes-its-usage-of-a-css-custom-property). + +## Alternatives + +### Define tokens on the component element with explicit assignment + +```css +.SegmentedControl { + --segmented-control-bgColor: var(--controlTrack-bgColor-rest); + background-color: var(--segmented-control-bgColor); +} +``` + +This is simpler to read and avoids repeated fallbacks, but **blocks CSS inheritance**. Tokens set on a parent element would be overridden by the component's own definition. This was the initial implementation for `SegmentedControl` and was rejected because the Storybook demo showed that parent overrides had no effect. + +### Use `@property` registration with `inherits: true` + +The CSS `@property` rule could register tokens with default values while preserving inheritance: + +```css +@property --segmented-control-bgColor { + syntax: ''; + inherits: true; + initial-value: transparent; +} +``` + +This was not chosen because `@property` has limited browser support for complex fallback chains (e.g., referencing other custom properties as initial values is not supported), and it adds complexity for minimal benefit over the `var()` fallback pattern. + +### Expose a `style` prop for each sub-element + +Instead of CSS custom properties, components could accept style objects for internal elements (e.g., `selectedStyle`, `hoverStyle`). This was rejected because it couples styling to the React API, does not support CSS inheritance, and significantly increases the prop surface of every component. From 20ae0a5a18ea42125968374f4d8be1f7714a14ec Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Fri, 27 Feb 2026 09:54:20 +0100 Subject: [PATCH 5/7] add adr --- .../adrs/adr-023-component-tokens.md | 121 ++++++++---------- 1 file changed, 56 insertions(+), 65 deletions(-) diff --git a/contributor-docs/adrs/adr-023-component-tokens.md b/contributor-docs/adrs/adr-023-component-tokens.md index b251f666e74..092f95cede4 100644 --- a/contributor-docs/adrs/adr-023-component-tokens.md +++ b/contributor-docs/adrs/adr-023-component-tokens.md @@ -4,18 +4,18 @@ ## Status -| Stage | State | -| -------------- | -------------- | -| Status | Proposed ❓ | -| Implementation | Not planned ⛔ | +| Stage | State | +| -------------- | -------- | +| Status | Proposed | +| Implementation | | ## Context -Primer React components use design tokens from `@primer/primitives` for colors, spacing, typography, and borders. These tokens enforce consistency across the design system but make it difficult for consumers to customize the appearance of individual component instances without resorting to fragile CSS overrides targeting internal class names. +Primer React components use design tokens from `@primer/primitives` for colors, spacing, typography, and borders. These tokens enforce consistency across the design system but make it difficult for consumers to customize the appearance of individual component instances without resorting to fragile CSS overrides targeting internal elements. -A common need is to restyle a component for a specific context — for example, rendering a `SegmentedControl` with accent colors to indicate a primary action area, or adjusting border radius to match a custom layout. Today, the only options are: +A common need is to restyle a component for a specific context or adjusting border radius to match a custom layout. Today, the only options are: -1. **Override internal CSS module class names** — brittle, breaks on refactors, and not part of the public API. +1. **Override internal CSS module class names or elements** — brittle, breaks on refactors, and not part of the public API. 2. **Use inline styles on the component** — limited to properties exposed on the root element and cannot target internal elements like the selected state or hover states. 3. **Wrap in a themed provider** — too broad, affects all components in the subtree. @@ -25,28 +25,36 @@ Component tokens solve this by exposing a stable set of CSS custom properties th Every component should expose a set of **component tokens** as CSS custom properties. These tokens are the public styling API of the component and follow a consistent pattern across all components. +Documentation must specify this as an escape hatch. + ### Naming convention Component tokens use the following naming pattern: ``` ---{component-name}-{property} ---{component-name}-{variant}-{property} +--{componentName}-{property} +--{componentName}-{variant}-{property} ``` Examples from `SegmentedControl`: -| Token | Purpose | -| ----------------------------------------- | ------------------------------------ | -| `--segmented-control-bgColor` | Track background color | -| `--segmented-control-bgColor-hover` | Track background on hover | -| `--segmented-control-borderColor` | Outer border color | -| `--segmented-control-borderRadius` | Outer border radius | -| `--segmented-control-fgColor-icon` | Icon color (default state) | -| `--segmented-control-fontWeight` | Text weight (default state) | -| `--segmented-control-selected-bgColor` | Selected segment background | -| `--segmented-control-selected-fgColor` | Selected segment text and icon color | -| `--segmented-control-selected-fontWeight` | Selected segment text weight | +| Token | Purpose | +| ------------------------------------------ | ------------------------------ | +| `--segmentedControl-bgColor` | Track background color | +| `--segmentedControl-bgColor-hover` | Track background on hover | +| `--segmentedControl-borderColor` | Outer border color | +| `--segmentedControl-borderRadius` | Outer border radius | +| `--segmentedControl-iconColor` | Icon color (default state) | +| `--segmentedControl-fgColor` | Text color (default state) | +| `--segmentedControl-iconColor-hover` | Icon color (hover state) | +| `--segmentedControl-fgColor-hover` | Text color (hover state) | +| `--segmentedControl-fontWeight` | Text weight (default state) | +| `--segmentedControl-selected-bgColor` | Selected segment background | +| `--segmentedControl-selected-fgColor` | Selected segment text color | +| `--segmentedControl-selected-iconColor` | Selected segment icon color | +| `--segmentedControl-selected-fontWeight` | Selected segment text weight | +| `--segmentedControl-selected-borderColor` | Selected element border color | +| `--segmentedControl-selected-borderRadius` | Selected element border radius | ### What to tokenize @@ -56,6 +64,7 @@ Expose tokens for visual properties that a consumer may reasonably need to custo - **Border radius** — the outer shape of the component. - **Font weight** — when the component uses non-standard weights (e.g., semibold for selected state). - **Spacing** — internal padding values that affect the component's visual density, when they are not derived from a standard size primitive. +- **Shadow** — shadows for internal elements and the main element ### What NOT to tokenize @@ -76,30 +85,22 @@ Do not expose tokens for: ```css .SegmentedControl { - --segmented-control-bgColor: var(--controlTrack-bgColor-rest); - background-color: var(--segmented-control-bgColor); + --segmentedControl-bgColor: var(--controlTrack-bgColor-rest); + background-color: var(--segmentedControl-bgColor); } ``` -With this pattern, setting `--segmented-control-bgColor` on a parent `
` has no effect because the component redefines it on its own element. +With this pattern, setting `--segmentedControl-bgColor` on a parent `
` has no effect because the component redefines it on its own element. ✅ **Correct — use `var()` with a fallback value:** ```css .SegmentedControl { - background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); + background-color: var(--segmentedControl-bgColor, var(--controlTrack-bgColor-rest)); } ``` -The token is never defined by the component. It only references it with a fallback. If a consumer sets `--segmented-control-bgColor` on any ancestor element, that value is inherited and used. If not, the fallback kicks in. - -For hardcoded fallback values (not from primitives), use the literal value directly: - -```css -.Button { - padding: var(--segmented-control-button-bg-inset, 4px); -} -``` +The token is never defined by the component. It only references it with a fallback. If a consumer sets `--segmentedControl-bgColor` on any ancestor element, that value is inherited and used. If not, the fallback kicks in. ### Documentation in CSS @@ -111,18 +112,18 @@ List all available component tokens in a comment block at the top of the compone * Component tokens – override these custom properties from a parent * element to customize the control: * - * --segmented-control-bgColor - * --segmented-control-bgColor-hover - * --segmented-control-bgColor-active - * --segmented-control-borderColor - * --segmented-control-borderRadius - * --segmented-control-selected-bgColor - * --segmented-control-selected-borderColor - * --segmented-control-selected-fgColor - * --segmented-control-selected-fontWeight + * --segmentedControl-bgColor + * --segmentedControl-bgColor-hover + * --segmentedControl-bgColor-active + * --segmentedControl-borderColor + * --segmentedControl-borderRadius + * --segmentedControl-selected-bgColor + * --segmentedControl-selected-borderColor + * --segmentedControl-selected-fgColor + * --segmentedControl-selected-fontWeight */ - background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); + background-color: var(--segmentedControl-bgColor, var(--controlTrack-bgColor-rest)); /* ... */ } ``` @@ -135,9 +136,9 @@ Consumers override tokens by setting them on a parent element or via inline styl
@@ -162,36 +163,26 @@ Consumers should prefer using Primer primitive values (e.g., `var(--bgColor-acce ### Negative -- **Repeated fallback values** — the same fallback (e.g., `var(--controlTrack-bgColor-rest)`) may appear in multiple places within a component's CSS, since tokens are not defined centrally on the element. -- **Stylelint noise** — component tokens trigger `primer/colors` and `primer/borders` lint rules because they are not recognized Primer primitives. Each usage requires a `stylelint-disable` comment. +- **Stylelint noise** — component tokens trigger `primer/colors` and `primer/borders` lint rules because they are not recognized Primer primitives. Each usage requires a `stylelint-disable` comment. This could be mitigated by allowing component tokens with primitives in styleLint. - **API surface** — once a token is documented and adopted, renaming or removing it is a breaking change per our [versioning policy](../versioning.md#a-component-changes-its-usage-of-a-css-custom-property). ## Alternatives -### Define tokens on the component element with explicit assignment +### Use `data-component` and `data-slot` attributes for styling hooks + +Components could expose stable DOM hooks such as `data-component="SegmentedControl"` and `data-slot="button"`, allowing consumers to target internal parts with selectors: ```css -.SegmentedControl { - --segmented-control-bgColor: var(--controlTrack-bgColor-rest); - background-color: var(--segmented-control-bgColor); +[data-component='SegmentedControl'] { + background-color: var(--bgColor-accent-muted); } -``` - -This is simpler to read and avoids repeated fallbacks, but **blocks CSS inheritance**. Tokens set on a parent element would be overridden by the component's own definition. This was the initial implementation for `SegmentedControl` and was rejected because the Storybook demo showed that parent overrides had no effect. -### Use `@property` registration with `inherits: true` - -The CSS `@property` rule could register tokens with default values while preserving inheritance: - -```css -@property --segmented-control-bgColor { - syntax: ''; - inherits: true; - initial-value: transparent; +[data-component='SegmentedControl'] [data-slot='selected'] { + color: var(--fgColor-onEmphasis); } ``` -This was not chosen because `@property` has limited browser support for complex fallback chains (e.g., referencing other custom properties as initial values is not supported), and it adds complexity for minimal benefit over the `var()` fallback pattern. +This relies on selector-based overrides, couples consumers to the component's internal DOM shape, and encourages state styling through external selectors rather than a constrained token contract. It can be useful for testing and diagnostics, but component tokens provide a clearer, inheritance-friendly, and more stable public styling API, that we can control with guradrails. ### Expose a `style` prop for each sub-element From b5aba931690dfa63eb3b47a3172cc80827159495 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Fri, 27 Feb 2026 10:15:43 +0100 Subject: [PATCH 6/7] refactor(SegmentedControl): align tokens with ADR-023 - Rename bgInset to trackPadding - Promote innerPadding and trackPadding to public tokens - Add bgColor-active token - Remove internal --iconWidth var, use direct width on .IconButton - Use camelCase naming throughout (--segmentedControl-*) - Separate iconColor / fgColor tokens for default and selected states - Add hover tokens for fgColor and iconColor - Update story with hover/active overrides using color-mix() - Update ADR-023 token table and code examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-component-tokens.md | 45 +++--- .../SegmentedControl.features.stories.tsx | 13 +- .../SegmentedControl.module.css | 151 ++++++++++-------- 3 files changed, 115 insertions(+), 94 deletions(-) diff --git a/contributor-docs/adrs/adr-023-component-tokens.md b/contributor-docs/adrs/adr-023-component-tokens.md index 092f95cede4..ec0d47421b6 100644 --- a/contributor-docs/adrs/adr-023-component-tokens.md +++ b/contributor-docs/adrs/adr-023-component-tokens.md @@ -38,23 +38,26 @@ Component tokens use the following naming pattern: Examples from `SegmentedControl`: -| Token | Purpose | -| ------------------------------------------ | ------------------------------ | -| `--segmentedControl-bgColor` | Track background color | -| `--segmentedControl-bgColor-hover` | Track background on hover | -| `--segmentedControl-borderColor` | Outer border color | -| `--segmentedControl-borderRadius` | Outer border radius | -| `--segmentedControl-iconColor` | Icon color (default state) | -| `--segmentedControl-fgColor` | Text color (default state) | -| `--segmentedControl-iconColor-hover` | Icon color (hover state) | -| `--segmentedControl-fgColor-hover` | Text color (hover state) | -| `--segmentedControl-fontWeight` | Text weight (default state) | -| `--segmentedControl-selected-bgColor` | Selected segment background | -| `--segmentedControl-selected-fgColor` | Selected segment text color | -| `--segmentedControl-selected-iconColor` | Selected segment icon color | -| `--segmentedControl-selected-fontWeight` | Selected segment text weight | -| `--segmentedControl-selected-borderColor` | Selected element border color | -| `--segmentedControl-selected-borderRadius` | Selected element border radius | +| Token | Purpose | +| ------------------------------------------ | ----------------------------------------- | +| `--segmentedControl-bgColor` | Track background color | +| `--segmentedControl-bgColor-hover` | Track background on hover | +| `--segmentedControl-bgColor-active` | Track background on active/press | +| `--segmentedControl-borderColor` | Outer border color | +| `--segmentedControl-borderRadius` | Outer border radius | +| `--segmentedControl-iconColor` | Icon color (default state) | +| `--segmentedControl-fgColor` | Text color (default state) | +| `--segmentedControl-iconColor-hover` | Icon color (hover state) | +| `--segmentedControl-fgColor-hover` | Text color (hover state) | +| `--segmentedControl-fontWeight` | Text weight (default state) | +| `--segmentedControl-innerPadding` | Horizontal padding inside each button | +| `--segmentedControl-trackPadding` | Inset between the track border and button | +| `--segmentedControl-selected-bgColor` | Selected segment background | +| `--segmentedControl-selected-fgColor` | Selected segment text color | +| `--segmentedControl-selected-iconColor` | Selected segment icon color | +| `--segmentedControl-selected-fontWeight` | Selected segment text weight | +| `--segmentedControl-selected-borderColor` | Selected element border color | +| `--segmentedControl-selected-borderRadius` | Selected element border radius | ### What to tokenize @@ -114,12 +117,18 @@ List all available component tokens in a comment block at the top of the compone * * --segmentedControl-bgColor * --segmentedControl-bgColor-hover - * --segmentedControl-bgColor-active * --segmentedControl-borderColor * --segmentedControl-borderRadius + * --segmentedControl-fgColor + * --segmentedControl-iconColor + * --segmentedControl-fontWeight + * --segmentedControl-innerPadding + * --segmentedControl-trackPadding * --segmentedControl-selected-bgColor * --segmentedControl-selected-borderColor + * --segmentedControl-selected-borderRadius * --segmentedControl-selected-fgColor + * --segmentedControl-selected-iconColor * --segmentedControl-selected-fontWeight */ diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx index e086c3e1454..e1535c1b3a3 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx @@ -160,11 +160,14 @@ export const CustomCSSTokens = () => (
diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 72fa983675f..f7d9600908e 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,26 +1,28 @@ .SegmentedControl { /* - * Component tokens – override these custom properties from a parent element to customize the control: + * Component tokens – override these custom properties from a parent + * element to customize the control: * - * --segmented-control-bgColor - * --segmented-control-bgColor-hover - * --segmented-control-bgColor-active - * --segmented-control-borderColor - * --segmented-control-borderRadius - * --segmented-control-fgColor-icon - * --segmented-control-fgColor-disabled - * --segmented-control-fontWeight - * --segmented-control-selected-bgColor - * --segmented-control-selected-borderColor - * --segmented-control-selected-fgColor - * --segmented-control-selected-fontWeight - * --segmented-control-button-inner-padding - * --segmented-control-button-bg-inset + * --segmentedControl-bgColor + * --segmentedControl-bgColor-hover + * --segmentedControl-bgColor-active + * --segmentedControl-borderColor + * --segmentedControl-borderRadius + * --segmentedControl-fgColor + * --segmentedControl-fgColor-hover + * --segmentedControl-iconColor + * --segmentedControl-iconColor-hover + * --segmentedControl-fontWeight + * --segmentedControl-innerPadding + * --segmentedControl-trackPadding + * --segmentedControl-selected-bgColor + * --segmentedControl-selected-fgColor + * --segmentedControl-selected-iconColor + * --segmentedControl-selected-fontWeight + * --segmentedControl-selected-borderColor + * --segmentedControl-selected-borderRadius */ - /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ - --segmented-control-icon-width: 32px; - display: inline-flex; /* TODO: use primitive `control.{small|medium}.size` when it is available */ @@ -29,27 +31,29 @@ margin: 0; font-size: var(--text-body-size-medium); /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor, var(--controlTrack-bgColor-rest)); + color: var(--segmentedControl-fgColor, currentColor); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--segmentedControl-bgColor, var(--controlTrack-bgColor-rest)); /* stylelint-disable primer/colors */ border: var(--borderWidth-thin) solid - var(--segmented-control-borderColor, var(--controlTrack-borderColor-rest, transparent)); + var(--segmentedControl-borderColor, var(--controlTrack-borderColor-rest, transparent)); /* stylelint-enable primer/colors */ /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); + border-radius: var(--segmentedControl-borderRadius, var(--borderRadius-medium)); /* Responsive full-width */ &[data-full-width='true'] { display: flex; width: 100%; - --segmented-control-icon-width: 100%; + & .IconButton { + width: 100%; + } } &[data-full-width='false'] { display: inline-flex; width: auto; - - --segmented-control-icon-width: 32px; } @media (--viewportRange-narrow) { @@ -57,14 +61,14 @@ display: flex; width: 100%; - --segmented-control-icon-width: 100%; + & .IconButton { + width: 100%; + } } &[data-full-width-narrow='false'] { display: inline-flex; width: auto; - - --segmented-control-icon-width: 32px; } } @@ -73,14 +77,14 @@ display: flex; width: 100%; - --segmented-control-icon-width: 100%; + & .IconButton { + width: 100%; + } } &[data-full-width-regular='false'] { display: inline-flex; width: auto; - - --segmented-control-icon-width: 32px; } } @@ -89,21 +93,19 @@ display: flex; width: 100%; - --segmented-control-icon-width: 100%; + & .IconButton { + width: 100%; + } } &[data-full-width-wide='false'] { display: inline-flex; width: auto; - - --segmented-control-icon-width: 32px; } &[data-full-width-regular='true']:not([data-full-width-wide='true']) { display: inline-flex; width: auto; - - --segmented-control-icon-width: 32px; } } @@ -236,23 +238,23 @@ width: 100%; height: 100%; /* stylelint-disable-next-line primer/spacing */ - padding: var(--segmented-control-button-bg-inset, 4px); + padding: var(--segmentedControl-trackPadding, 4px); font-family: inherit; font-size: inherit; - font-weight: var(--segmented-control-fontWeight, var(--base-text-weight-normal)); - color: currentColor; + font-weight: var(--segmentedControl-fontWeight, var(--base-text-weight-normal)); + color: inherit; cursor: pointer; background-color: transparent; border-color: transparent; border-width: 0; /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); + border-radius: var(--segmentedControl-borderRadius, var(--borderRadius-medium)); & svg { /* stylelint-disable-next-line primer/colors */ - fill: var(--segmented-control-fgColor-icon, var(--fgColor-muted)); + fill: var(--segmentedControl-iconColor, var(--fgColor-muted)); /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-icon, var(--fgColor-muted)); + color: var(--segmentedControl-iconColor, var(--fgColor-muted)); } /* fallback :focus state */ @@ -282,15 +284,12 @@ &[aria-disabled='true']:not([aria-current='true']) { cursor: not-allowed; - /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); + color: var(--fgColor-disabled); background-color: transparent; & svg { - /* stylelint-disable-next-line primer/colors */ - fill: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); - /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-fgColor-disabled, var(--fgColor-disabled)); + fill: var(--fgColor-disabled); + color: var(--fgColor-disabled); } } @@ -308,21 +307,16 @@ } .IconButton { - width: var(--segmented-control-icon-width, 32px); + /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ + width: 32px; } .Content { display: flex; height: 100%; /* stylelint-disable primer/spacing */ - padding-right: calc( - var(--segmented-control-button-inner-padding, var(--base-size-12)) - - var(--segmented-control-button-bg-inset, var(--base-size-4)) - ); - padding-left: calc( - var(--segmented-control-button-inner-padding, var(--base-size-12)) - - var(--segmented-control-button-bg-inset, var(--base-size-4)) - ); + padding-right: calc(var(--segmentedControl-innerPadding, 12px) - var(--segmentedControl-trackPadding, 4px)); + padding-left: calc(var(--segmentedControl-innerPadding, 12px) - var(--segmentedControl-trackPadding, 4px)); /* stylelint-enable primer/spacing */ border-color: transparent; border-style: solid; @@ -334,8 +328,7 @@ */ /* stylelint-disable primer/borders */ border-radius: calc( - var(--segmented-control-borderRadius, var(--borderRadius-medium)) - var(--segmented-control-button-bg-inset, 4px) / - 2 + var(--segmentedControl-borderRadius, var(--borderRadius-medium)) - var(--segmentedControl-trackPadding, 4px) / 2 ); /* stylelint-enable primer/borders */ align-items: center; @@ -344,40 +337,56 @@ .Button[aria-current='true'] { padding: 0; - font-weight: var(--segmented-control-selected-fontWeight, var(--base-text-weight-semibold)); + font-weight: var(--segmentedControl-selected-fontWeight, var(--base-text-weight-semibold)); /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-selected-fgColor, inherit); + color: var(--segmentedControl-selected-fgColor, inherit); & svg { /* stylelint-disable-next-line primer/colors */ - fill: var(--segmented-control-selected-fgColor, inherit); + fill: var(--segmentedControl-selected-iconColor, inherit); /* stylelint-disable-next-line primer/colors */ - color: var(--segmented-control-selected-fgColor, inherit); + color: var(--segmentedControl-selected-iconColor, inherit); } .Content { /* stylelint-disable-next-line primer/spacing */ - padding-right: var(--segmented-control-button-inner-padding, 12px); + padding-right: var(--segmentedControl-innerPadding, 12px); /* stylelint-disable-next-line primer/spacing */ - padding-left: var(--segmented-control-button-inner-padding, 12px); + padding-left: var(--segmentedControl-innerPadding, 12px); /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-selected-bgColor, var(--controlKnob-bgColor-rest)); + background-color: var(--segmentedControl-selected-bgColor, var(--controlKnob-bgColor-rest)); /* stylelint-disable-next-line primer/colors */ - border-color: var(--segmented-control-selected-borderColor, var(--controlKnob-borderColor-rest)); - /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-borderRadius, var(--borderRadius-medium)); + border-color: var(--segmentedControl-selected-borderColor, var(--controlKnob-borderColor-rest)); + /* stylelint-disable primer/borders */ + border-radius: var( + --segmentedControl-selected-borderRadius, + var(--segmentedControl-borderRadius, var(--borderRadius-medium)) + ); + /* stylelint-enable primer/borders */ } } .Button:not([aria-current='true'], [aria-disabled='true']) { + &:hover { + /* stylelint-disable-next-line primer/colors */ + color: var(--segmentedControl-fgColor-hover, inherit); + + & svg { + /* stylelint-disable-next-line primer/colors */ + fill: var(--segmentedControl-iconColor-hover, var(--segmentedControl-iconColor, var(--fgColor-muted))); + /* stylelint-disable-next-line primer/colors */ + color: var(--segmentedControl-iconColor-hover, var(--segmentedControl-iconColor, var(--fgColor-muted))); + } + } + &:hover .Content { /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor-hover, var(--controlTrack-bgColor-hover)); + background-color: var(--segmentedControl-bgColor-hover, var(--controlTrack-bgColor-hover)); } &:active .Content { /* stylelint-disable-next-line primer/colors */ - background-color: var(--segmented-control-bgColor-active, var(--controlTrack-bgColor-active)); + background-color: var(--segmentedControl-bgColor-active, var(--controlTrack-bgColor-active)); } } @@ -385,7 +394,7 @@ display: block; height: 0; overflow: hidden; - font-weight: var(--segmented-control-selected-fontWeight, var(--base-text-weight-semibold)); + font-weight: var(--segmentedControl-selected-fontWeight, var(--base-text-weight-semibold)); pointer-events: none; visibility: hidden; content: attr(data-text); From f9faa5026f27b1384b6c472e8e059066188baf90 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Fri, 27 Feb 2026 16:24:55 +0100 Subject: [PATCH 7/7] docs(ADR-023): replace CSS comment block with docs.json documentation Remove the inline CSS comment listing all tokens. Instead, require component tokens to be documented in the component's docs.json file as a cssTokens array. Note future tooling opportunities: a stylelint plugin to enforce naming conventions and a PostCSS plugin to extract tokens into docs.json automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adrs/adr-023-component-tokens.md | 45 ++++++++----------- .../SegmentedControl.module.css | 24 ---------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/contributor-docs/adrs/adr-023-component-tokens.md b/contributor-docs/adrs/adr-023-component-tokens.md index ec0d47421b6..bc5557f96af 100644 --- a/contributor-docs/adrs/adr-023-component-tokens.md +++ b/contributor-docs/adrs/adr-023-component-tokens.md @@ -105,38 +105,29 @@ With this pattern, setting `--segmentedControl-bgColor` on a parent `
` has The token is never defined by the component. It only references it with a fallback. If a consumer sets `--segmentedControl-bgColor` on any ancestor element, that value is inherited and used. If not, the fallback kicks in. -### Documentation in CSS +### Documentation -List all available component tokens in a comment block at the top of the component's root selector: +Component tokens must be documented in the component's `*.docs.json` file as a `cssTokens` array. This makes them discoverable through the documentation system and enables automated tooling. -```css -.SegmentedControl { - /* - * Component tokens – override these custom properties from a parent - * element to customize the control: - * - * --segmentedControl-bgColor - * --segmentedControl-bgColor-hover - * --segmentedControl-borderColor - * --segmentedControl-borderRadius - * --segmentedControl-fgColor - * --segmentedControl-iconColor - * --segmentedControl-fontWeight - * --segmentedControl-innerPadding - * --segmentedControl-trackPadding - * --segmentedControl-selected-bgColor - * --segmentedControl-selected-borderColor - * --segmentedControl-selected-borderRadius - * --segmentedControl-selected-fgColor - * --segmentedControl-selected-iconColor - * --segmentedControl-selected-fontWeight - */ - - background-color: var(--segmentedControl-bgColor, var(--controlTrack-bgColor-rest)); - /* ... */ +```json +{ + "id": "segmented_control", + "name": "SegmentedControl", + "cssTokens": [ + { + "name": "--segmentedControl-bgColor", + "defaultValue": "var(--controlTrack-bgColor-rest)", + "description": "Track background color" + } + ] } ``` +**Future tooling considerations:** + +- A **stylelint plugin** could enforce that undefined custom properties (those not from `@primer/primitives`) follow the `--{componentName}-{property}` naming convention. +- A **PostCSS plugin** could extract component tokens from CSS files and generate the `cssTokens` entries in `docs.json` automatically, keeping documentation in sync with the implementation. + ### Consumer usage Consumers override tokens by setting them on a parent element or via inline styles: diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index f7d9600908e..63b92ac8f5a 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,28 +1,4 @@ .SegmentedControl { - /* - * Component tokens – override these custom properties from a parent - * element to customize the control: - * - * --segmentedControl-bgColor - * --segmentedControl-bgColor-hover - * --segmentedControl-bgColor-active - * --segmentedControl-borderColor - * --segmentedControl-borderRadius - * --segmentedControl-fgColor - * --segmentedControl-fgColor-hover - * --segmentedControl-iconColor - * --segmentedControl-iconColor-hover - * --segmentedControl-fontWeight - * --segmentedControl-innerPadding - * --segmentedControl-trackPadding - * --segmentedControl-selected-bgColor - * --segmentedControl-selected-fgColor - * --segmentedControl-selected-iconColor - * --segmentedControl-selected-fontWeight - * --segmentedControl-selected-borderColor - * --segmentedControl-selected-borderRadius - */ - display: inline-flex; /* TODO: use primitive `control.{small|medium}.size` when it is available */