[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 24 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.
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!
.../bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.module.scss
Outdated
Show resolved
Hide resolved
.../bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.module.scss
Show resolved
Hide resolved
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
…ner/backpack into 001-composable-segmented-control
|
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. |
There was a problem hiding this comment.
Code review
Found 2 questions:
-
bpk-segmented-control-v2--canvas-defaultmodifier is a no-op — the base mixinbpk-segmented-control-v2already initialises--bpk-segmented-control-bgtotokens.$bpk-private-segmented-control-canvas-default-dayon line 38. The modifier on lines 63–65 overwrites the same variable with the identical value, making it dead code. Future maintainers will expect this modifier to have an effect and be confused when it doesn't. Either remove the modifier (and document that the default type is always active via the base mixin), or change it to/* inherits base defaults — canvas-default is the component default */to make the intent explicit.backpack/packages/bpk-mixins/_segmented-control.scss
Lines 63 to 65 in 207815a
-
nit: PR description references "disabled states" that no longer exist — the description says the Storybook stories cover "disabled states" but a commit on this branch titled
remove disable stateremoved the feature. There is nodisabledprop inBpkSegmentedControlV2ItemPropsorBpkSegmentedControlV2RootProps, and no disabled story or example in the stories file. The PR description should be updated to remove the disabled-states claim to avoid misleading reviewers and future readers of the changelog.
🤖 Generated with Claude Code
|
Hi Ezreal Yang (@Supremeyh), I reviewed the examples on the storybook link and found 2 small questions:
- **nit**: Screen reader question: it tells the whole Segmented Item is a radio. I feel like it may not be a right role for screen reader.
|
But this does not conform to the WAI-ARIA Radio Group specification. The SegmentGroup of Ark-UI is essentially role="radiogroup" + role="radio", and the correct keyboard interaction mode is roving tabindex:
The container of
|
|
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. |





Summary
Introduces
BpkSegmentedControlV2as an experimental composable segmented control alongside the existing V1 component (no breaking changes).The component is rebuilt on Ark-UI
SegmentGroup, replacing thebuttonContentsarray API with a dot-notation composable API:Changes
New component —
BpkSegmentedControlV2packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx— composableRoot+Item.RootusesChildren.mapto drive Ark-UISegmentGrouprendering; keyboard navigation (Arrow/Home/End/Space/Enter) and RTL handled internally.BpkSegmentedControlV2.module.scss— CSS module wiring up mixins frombpk-mixins/_segmented-control.scss; item padding overridable via--bpk-segmented-control-padding-y/--bpk-segmented-control-padding-x.common-types.ts—BpkSegmentedControlV2RootProps,BpkSegmentedControlV2ItemProps,SegmentTypesV2type,SEGMENT_TYPES_V2enum.BpkSegmentedControlV2-test.tsx— 29 assertion-based unit tests across 5 groups: basic rendering, keyboard navigation (automatic & manual activation modes), CSS variable theming, style variants (snapshots), and composable custom content.accessibility-test.tsx— 6 jest-axe tests covering all type variants.BpkSegmentedControlV2.figma.tsx— Figma Code Connect mapping.New SCSS mixin module —
bpk-mixins/_segmented-control.scss13 new public mixins extracted from the component's styles, making the styling available to non-CSS-module consumers:
bpk-segmented-control-v2bpk-segmented-control-v2--canvas-defaultbpk-segmented-control-v2--canvas-contrastbpk-segmented-control-v2--surface-defaultbpk-segmented-control-v2--surface-contrastbpk-segmented-control-v2--shadowbpk-segmented-control-v2__labelbpk-segmented-control-v2__itembpk-segmented-control-v2__item-controlbpk-segmented-control-v2__item-control--canvas-contrastbpk-segmented-control-v2__item-control--surface-defaultbpk-segmented-control-v2__item-control--surface-contrastbpk-segmented-control-v2__item-textPackage & exports
index.ts— exportsBpkSegmentedControlV2,SEGMENT_TYPES_V2, and all TypeScript types alongside unchanged V1 exports.README.md— V2 usage, props table, CSS custom properties, and V1→V2 migration guide.packages/package.json— adds@ark-ui/react ^5.34.1.Storybook (
examples/bpk-component-segmented-control-v2/)Stories covering: all 4 type variants, shadow, icon+text, icon-only, RTL, edge cases (2 items / long labels / no initial selection), complex content with icon, complex content with button in slot, and an interactive type-switcher story.
CSS custom properties
Two public properties for consumer padding customisation:
--bpk-segmented-control-padding-ybpk-spacing-sm()(0.5rem)--bpk-segmented-control-padding-xbpk-spacing-md()(1.25rem)V1 → V2 migration quick-reference
buttonContents={['A', 'B']}<BpkSegmentedControlV2.Item>childrenselectedIndex={0}value="a"onItemClick={(i) => fn(i)}onChange={(v) => fn(v)}type={SEGMENT_TYPES.CanvasDefault}type={SEGMENT_TYPES_V2.CanvasDefault}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