[CLOV-1326] [BpkSegmentedControl] Modernize BpkSegmentedControlV2 composable rebase on Ark-UI#4257
[CLOV-1326] [BpkSegmentedControl] Modernize BpkSegmentedControlV2 composable rebase on Ark-UI#4257Ezreal Yang (Supremeyh) wants to merge 13 commits intomainfrom
Conversation
… & Rebase on Ark-UI segmentGroup
There was a problem hiding this comment.
Pull request overview
This PR introduces BpkSegmentedControlV2, a new composable segmented control built on Ark-UI SegmentGroup, shipped alongside the existing V1 component (which remains the default export). It also adds supporting specs, tests, Storybook examples, and documentation for the new V2 API.
Changes:
- Add new
BpkSegmentedControlV2implementation (TS + SCSS) with accessibility/unit tests and Figma Code Connect file. - Publish V2 via named exports and document the new API (README + Storybook examples/stories).
- Add Ark-UI as a dependency and include extensive design/spec documentation under
specs/.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
specs/001-composable-segmented-control/tasks.md |
Task breakdown for implementing V2 (phases, tests, docs, migration). |
specs/001-composable-segmented-control/styling-guide.md |
Styling and token/CSS-variable guidance for V2. |
specs/001-composable-segmented-control/spec.md |
Product/requirements spec for V2 behavior and constraints. |
specs/001-composable-segmented-control/research.md |
Research notes and decisions (Ark-UI usage, theming approach, etc.). |
specs/001-composable-segmented-control/plan.md |
Implementation plan and file structure for V2 rollout. |
specs/001-composable-segmented-control/checklists/requirements.md |
Spec quality checklist for the feature. |
specs/001-composable-segmented-control/api-design.md |
Proposed API/types/component shape and export strategy. |
packages/package.json |
Adds @ark-ui/react dependency to the published package set. |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/common-types.ts |
Defines V2 public types and SEGMENT_TYPES_V2. |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/accessibility-test.tsx |
Adds jest-axe accessibility coverage for V2 scenarios. |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx |
Implements Root + Item composable API on Ark SegmentGroup. |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.module.scss |
V2 styling (CSS variables, variants, focus, divider logic). |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.figma.tsx |
Adds Figma Code Connect integration for V2. |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2-test.tsx |
Unit tests for V2 rendering, keyboard, variants, and theming. |
packages/bpk-component-segmented-control/index.ts |
Exposes V2 as named exports while keeping V1 default export. |
packages/bpk-component-segmented-control/README.md |
Documents the experimental V2 API and migration notes. |
examples/bpk-component-segmented-control-v2/stories.tsx |
Storybook stories for V2 variants/states. |
examples/bpk-component-segmented-control-v2/examples.tsx |
Story implementations demonstrating V2 usage and edge cases. |
examples/bpk-component-segmented-control-v2/examples.module.scss |
Storybook-specific styling for examples. |
.eslintrc |
Allows Ark-UI SegmentGroup subcomponents to receive className. |
Files not reviewed (1)
- packages/package-lock.json: Language not supported
Comments suppressed due to low confidence (9)
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx:109
- For
Home/End(and potentially Arrow keys when only one enabled item exists),onChangecan be called even when the newly computed index equals the current index. That can cause redundantonChangecalls compared to mouse behaviour. Consider guarding soonChange/selection only fires when the target value differs from the currently focused/selected input.
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = lastIndex;
break;
case ' ':
case 'Enter':
if (activationMode === 'manual') {
event.preventDefault();
onChange?.(inputs[currentIndex].value);
}
return;
default:
return;
}
event.preventDefault();
inputs[newIndex].focus();
if (activationMode !== 'manual') {
onChange?.(inputs[newIndex].value);
}
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2-test.tsx:363
- The US3 theming test only asserts that the wrapper element has a CSS custom property set; it doesn’t verify the component actually consumes the variable (e.g., that the root background or checked item background resolves to it). Please add an assertion on the rendered segmented control element styles (computed style where feasible, or at least presence of the expected
var(--bpk-segmented-control-bg)usage on the relevant element).
describe('BpkSegmentedControlV2 — US3: CSS variable theming', () => {
it('renders root class that CSS variables can be read from', () => {
const { container } = render(<ThreeItemControl />);
const root = container.querySelector('[class*="bpk-segmented-control-v2"]');
expect(root).toBeInTheDocument();
});
it('wrapper CSS variable override is applied to the root element context', () => {
const { container } = render(
<div style={{ '--bpk-segmented-control-bg': 'red' } as CSSProperties}>
<ThreeItemControl />
</div>,
);
const wrapper = container.firstElementChild as HTMLElement;
expect(wrapper.style.getPropertyValue('--bpk-segmented-control-bg')).toBe('red');
});
});
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.module.scss:38
- The default
--bpk-segmented-control-bgtoken is set totokens.$bpk-surface-default-day, but V1’scanvas-defaultusestokens.$bpk-private-segmented-control-canvas-default-day(and the README/research docs reference the private token). If V2 is meant to be visually indistinguishable from V1 for the sametype, the token mapping here likely needs updating (or docs need to be corrected consistently).
.bpk-segmented-control-v2 {
// Default theme: canvas-default — all CSS variables set from tokens.
// Consumers (or type variant modifiers) override these to change the theme.
--bpk-segmented-control-bg: #{tokens.$bpk-surface-default-day};
--bpk-segmented-control-item-color: #{tokens.$bpk-text-primary-day};
--bpk-segmented-control-item-disabled-color: #{tokens.$bpk-text-disabled-day};
--bpk-segmented-control-indicator-bg: #{tokens.$bpk-core-primary-day};
--bpk-segmented-control-indicator-color: #{tokens.$bpk-text-on-dark-day};
--bpk-segmented-control-border-radius: #{tokens.$bpk-border-radius-sm};
--bpk-segmented-control-padding-x: #{tokens.bpk-spacing-base()};
--bpk-segmented-control-padding-y: #{tokens.bpk-spacing-md()};
--bpk-segmented-control-divider-color: #{tokens.$bpk-line-day};
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.module.scss:23
radiiandshadowsare imported via@usebut not referenced anywhere in this stylesheet. Please remove unused Sass modules to keep the file minimal and avoid implying that these mixins are being applied.
@use '../../../bpk-mixins/tokens';
@use '../../../bpk-mixins/utils';
@use '../../../bpk-mixins/typography';
@use '../../../bpk-mixins/radii';
@use '../../../bpk-mixins/shadows';
packages/bpk-component-segmented-control/README.md:166
- The CSS custom properties table is missing
--bpk-segmented-control-shadow, which is part of the public theming API in the specs and is used by theshadowmodifier inBpkSegmentedControlV2.module.scss. Please document it here (and ensure the listed default tokens match the actual SCSS defaults, e.g.--bpk-segmented-control-bg).
### CSS custom properties
Override any of these on a wrapper element to theme the component:
| Property | Default | Controls |
|---|---|---|
| `--bpk-segmented-control-bg` | `$bpk-private-segmented-control-canvas-default-day` | Group background |
| `--bpk-segmented-control-item-color` | `$bpk-text-primary-day` | Unselected item text/icon |
| `--bpk-segmented-control-item-disabled-color` | `$bpk-text-disabled-day` | Disabled item text/icon |
| `--bpk-segmented-control-indicator-bg` | `$bpk-core-primary-day` | Selected item background |
| `--bpk-segmented-control-indicator-color` | `$bpk-text-on-dark-day` | Selected item text/icon |
| `--bpk-segmented-control-border-radius` | `$bpk-border-radius-sm` | Group + item corner radius |
| `--bpk-segmented-control-padding-x` | `1rem` | Horizontal item padding |
| `--bpk-segmented-control-padding-y` | `1.25rem` | Vertical item padding |
| `--bpk-segmented-control-divider-color` | `$bpk-line-day` | Divider between items |
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2-test.tsx:382
- This parameterised test doesn’t actually assert the specific modifier passed in via
expectedClass(it matches any of the variants), so it can pass even iftypehandling is broken for one variant. Please assert that the root className contains the expected modifier for the currenttypevalue.
it.each([
[SEGMENT_TYPES_V2.CanvasDefault, 'bpk-segmented-control-v2--canvas-default'],
[SEGMENT_TYPES_V2.CanvasContrast, 'bpk-segmented-control-v2--canvas-contrast'],
[SEGMENT_TYPES_V2.SurfaceDefault, 'bpk-segmented-control-v2--surface-default'],
[SEGMENT_TYPES_V2.SurfaceContrast, 'bpk-segmented-control-v2--surface-contrast'],
])('type="%s" adds BEM modifier class "%s"', (type, expectedClass) => {
const { container } = render(
<BpkSegmentedControlV2.Root label="Test" type={type}>
<BpkSegmentedControlV2.Item value="a">A</BpkSegmentedControlV2.Item>
</BpkSegmentedControlV2.Root>,
);
const root = container.firstChild as HTMLElement;
// CSS modules transform class names; check for a class that includes the modifier
expect(root.className).toMatch(/canvas-default|canvas-contrast|surface-default|surface-contrast/);
});
specs/001-composable-segmented-control/styling-guide.md:22
- This styling guide says
shadowsis no longer imported (and the shadow is a CSS variable), but the implemented SCSS currently still@usesshadows(and doesn’t reference it). Please update this guide to match the implementation (or adjust the implementation to match the guide).
> `shadows` is no longer imported — the shadow is now a CSS custom property (`--bpk-segmented-control-shadow`) rather than a mixin include, making it consumer-overridable.
>
> Import paths are relative from `src/BpkSegmentedControlV2/` to `packages/bpk-mixins/`.
examples/bpk-component-segmented-control-v2/examples.module.scss:24
- This stylesheet defines
.bpk-component-segmented-control-stories__custom-button, but it isn’t referenced anywhere in the V2 examples/stories. If it’s leftover from an earlier iteration, consider removing it to avoid dead CSS in the Storybook bundle.
.bpk-component-segmented-control-stories__custom-button {
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
specs/001-composable-segmented-control/tasks.md:94
- This tasks doc mentions declaring “all 9 CSS custom properties” but the styling guide/spec define 10 (including
--bpk-segmented-control-shadow). Please align the count and ensure the task list matches the agreed public theming API.
- Apache 2.0 license header
- Sass imports: `@use '../../../bpk-mixins/tokens'`, `utils`, `typography`, `radii`, `shadows`
- `.bpk-segmented-control-v2` root: declare all 9 CSS custom properties with Backpack token defaults (per `styling-guide.md` §3–4); `display: grid; grid-auto-columns: 1fr; grid-auto-flow: column; overflow: hidden`; `background-color: var(--bpk-segmented-control-bg)`; `border-radius: var(--bpk-segmented-control-border-radius)`
- `&--canvas-contrast`, `&--surface-default`, `&--surface-contrast`, `&--shadow` modifier blocks (all CSS variable overrides per `styling-guide.md` §6; shadow uses `@include shadows.bpk-box-shadow-sm`)
- `.bpk-segmented-control-v2__item`: `display: contents`; `cursor: pointer`; `&[data-disabled]` cursor
- `.bpk-segmented-control-v2__item-control`: flex centering; `min-height: tokens.bpk-spacing-xl()`; padding from CSS vars; `@include typography.bpk-label-2`; `border-inline-start` divider (logical property); `&[data-state='checked']` selected styles; `&[data-disabled]` disabled styles; `&:focus-visible { @include utils.bpk-focus-indicator }`
- First/last child border-radius via logical properties: `border-start-start-radius`, `border-end-start-radius`, `border-start-end-radius`, `border-end-end-radius`
- `.bpk-segmented-control-v2__item-text`: flex + gap; `white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none`
- **Constitution Check**: `@use` only (no `@import`); all sizing in rem via tokens; BEM with `bpk-` prefix; logical CSS properties for RTL
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx
Outdated
Show resolved
Hide resolved
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx
Outdated
Show resolved
Hide resolved
...es/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.figma.tsx
Outdated
Show resolved
Hide resolved
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
Code reviewFound 3 issues:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
thanks your review, address in onChange and label required |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4257 to see this build running in a browser. |
Gert-Jan Vercauteren (gert-janvercauteren)
left a comment
There was a problem hiding this comment.
Let me review before you hit merge.
| ); | ||
| }; | ||
|
|
||
| const WithShadow = () => { |
There was a problem hiding this comment.
We should not have a shadow variant, the Figma design does not have this option
There was a problem hiding this comment.
i add it as the figma mcp says need a inset shadow, and v1 has shadow prop and effect
Gert-Jan Vercauteren (gert-janvercauteren)
left a comment
There was a problem hiding this comment.
Great to see SegmentedControl get the composable treatment. There's a few things we can tighten up before merging!
Thanks for all the effort so far!
| ); | ||
| }; | ||
|
|
||
| const WithShadow = () => { |
| }; | ||
|
|
||
| const RootDisabled = () => ( | ||
| <BpkSegmentedControlV2.Root |
| value={selected} | ||
| onChange={setSelected} | ||
| > | ||
| <BpkSegmentedControlV2.Item value="grid" accessibilityLabel="Grid view"> |
There was a problem hiding this comment.
nit: maybe we can use BpkVisuallyHidden instead of accessibilityLabel?
If we want to have this label, I would suggest supporting aria-label
| { | ||
| '--bpk-segmented-control-bg': '#e8f0fe', | ||
| '--bpk-segmented-control-indicator-bg': '#0066cc', | ||
| '--bpk-segmented-control-indicator-color': '#ffffff', | ||
| '--bpk-segmented-control-item-color': '#1a1a2e', | ||
| '--bpk-segmented-control-border-radius': '0.5rem', | ||
| '--bpk-segmented-control-divider-color': '#c0cbdb', | ||
| } as CSSProperties |
There was a problem hiding this comment.
Style overrides should not be supported like this and only through BpkThemeProvider.
There was a problem hiding this comment.
The VDL override story does not show visual difference compared to the original.
| const LongLabels = () => { | ||
| const [selected, setSelected] = useState('departure'); | ||
| return ( | ||
| <div style={{ width: '300px' }}> |
| opacity: 0.5; | ||
| } | ||
|
|
||
| &:focus-visible { |
There was a problem hiding this comment.
In the storybook I cannot see the focus indicator. This should be fixed
| @use '../../../bpk-mixins/typography'; | ||
| @use '../../../bpk-mixins/radii'; | ||
| @use '../../../bpk-mixins/shadows'; | ||
|
|
There was a problem hiding this comment.
The structure of the SCSS should follow BEM naming and match our other CSS setups.
| const extractTextContent = (node: ReactNode): string => { | ||
| if (typeof node === 'string' || typeof node === 'number') return String(node); | ||
| if (Array.isArray(node)) return node.map(extractTextContent).join(' ').trim(); | ||
| if (isValidElement(node)) { | ||
| const element = node as ReactElement<{ children?: ReactNode }>; | ||
| if (element.props.children) | ||
| return extractTextContent(element.props.children); | ||
| } | ||
| return ''; | ||
| }; |
There was a problem hiding this comment.
How does this work when consumers pass in a layout component? On Figma there is an example where we have also icons and complex layout together.
| <SegmentGroup.ItemText | ||
| className={getClassName('bpk-segmented-control-v2__item-text')} | ||
| > | ||
| {item.props.children} | ||
| </SegmentGroup.ItemText> |
There was a problem hiding this comment.
I think we only want ItemText when a simple string is passed. In any other case we just use the stuff consumers pass?
| }; | ||
| ``` | ||
|
|
||
| ### CSS custom properties |
There was a problem hiding this comment.
This should not be part of the README for now



Summary
BpkSegmentedControlV2as an experimental composable segmented control alongside the existing V1 component (no breaking changes).SegmentGroup, replacing thebuttonContentsarray API with a dot-notation composable API:<BpkSegmentedControlV2.Root>+<BpkSegmentedControlV2.Item>.@ark-ui/reactto^5.34.1(latest stable, API-compatible with previous version in use).Changes
New component (
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/):BpkSegmentedControlV2.tsx— composable root + props-only item placeholder; all Ark-UI rendering handled inRootviaChildren.mapBpkSegmentedControlV2.module.scss— 10 CSS custom properties; inset shadow via--bpk-segmented-control-shadow; state transition onbackground-color,color, andborder-inline-start-color; divider suppression via CSS variable inheritance throughdisplay:contentscommon-types.ts— TypeScript types andSEGMENT_TYPES_V2enumBpkSegmentedControlV2-test.tsx— 38 assertion-based unit tests (no snapshots)accessibility-test.tsx— jest-axe tests covering all variants and keyboard navigationBpkSegmentedControlV2.figma.tsx— Figma Code Connect mappingPackage updates:
index.ts— adds V2 experimental exports alongside V1 (V1 unchanged)README.md— adds V2 usage, CSS custom properties table, and V1→V2 migration guidepackages/package.json— add@ark-ui/reactwith version^5.34.1Storybook (
examples/bpk-component-segmented-control-v2/):Snapshots
Testing
Check with figma mcp
Remember to include the following changes:
[Clover-123][BpkButton] Updating the colourREADME.md(If you have created a new component)README.md