[CLOV-1326] [BpkSegmentedControl] Rebase BpkSegmentedControl With Ark-UI#4257
[CLOV-1326] [BpkSegmentedControl] Rebase BpkSegmentedControl With Ark-UI#4257Ezreal Yang (Supremeyh) merged 27 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. |
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. |
|
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.
There's a few things we can adjust to ensure we match the Ark-ui structure
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
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx
Outdated
Show resolved
Hide resolved
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx
Show resolved
Hide resolved
packages/bpk-component-segmented-control/src/BpkSegmentedControlV2/BpkSegmentedControlV2.tsx
Show resolved
Hide resolved
|
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.
PR looks good to me. In a follow-up we can add themeability :D
| | `onItemClick={(i) => fn(i)}` | `onChange={(v) => fn(v)}` | | ||
| | `type={SEGMENT_TYPES.CanvasDefault}` | `type={SEGMENT_TYPES_V2.CanvasDefault}` | | ||
|
|
||
| ### BpkSegmentedControlV2.Root props |
There was a problem hiding this comment.
nit: we can remove props from the README
There was a problem hiding this comment.
PR looks good to me. In a follow-up we can add themeability :D
okay, will update in next PR





Summary
https://skyscanner.atlassian.net/browse/CLOV-1326
Add
BpkSegmentedControlV2— composable Ark UI segmented controlArchitecture
BpkSegmentedControlV2is a composable segmented control built on Ark-UI SegmentGroup, which is powered by the Zag-JS radio group state machine. The component exposes Ark-UI's sub-components as named slots on a single namespace object.DOM structure
Selection indicator
The floating
Indicatorreplaces the old per-itemItemControlbackground approach.Zag-JS measures the selected
Item's bounding rect and injects four inline CSS variables onto theIndicatorelement:--left--top--width--heightThe indicator reads these via
left: var(--left)etc. on anposition: absoluteelement inside theposition: relativeroot.--transition-durationis a Zag-JS animation control variable. We set it to0msto disable the sliding animation.Keyboard navigation
All keyboard behaviour is handled natively by Ark-UI's radio group state machine:
ArrowRight/ArrowDownArrowLeft/ArrowUpHomeEndRTL is also handled by Ark-UI. No custom
onKeyDownis needed.Dividers
Segment dividers are
border-inline-startonItemText(logical CSS, RTL-aware). They are hidden in two cases::first-of-type .item-text { border-inline-start: none }:has([data-state='checked'])selectorsItemControlholdsdata-state="checked"even though it isdisplay: none, so the:hasselectors still resolve correctly.Theming
Color values use design tokens directly (e.g.
tokens.$bpk-core-primary-day). Thebpk-themeable-propertymixin andthemeAttributesare not implemented yet — this is planned for a follow-up PR, aligned with the Backpack theming standard used by other components (e.g.BpkBadge).Per-type indicator colors:
typebackground-colorcanvas-default$bpk-core-primary-daycanvas-contrast$bpk-surface-default-daysurface-default$bpk-canvas-contrast-daysurface-contrast$bpk-private-segmented-control-surface-contrast-on-dayV1 → V2 migration quick-reference
API mapping
<BpkSegmentedControl /><BpkSegmentedControlV2.Root>buttonContents={['Price', 'Rating']}<Item value="price"><ItemText>Price</ItemText>…</Item>Itemper segmentselectedIndex={0}value="price"onItemClick={(i) => fn(i)}onChange={(v) => fn(v)}type={SEGMENT_TYPES.CanvasDefault}type={SEGMENT_TYPES_V2.CanvasDefault}SEGMENT_TYPES_V2shadowshadowactivationMode<BpkSegmentedControlV2.Indicator />RootBefore / after
Key differences
onChangegives you back the samevaluestring you passed toItem, not a numeric position.Indicatoris explicit — the floating selection background must be placed as the first child ofRoot. Omitting it renders no selection highlight.ItemControlandItemHiddenInputare required slots — even though they render nothing visible, they must be present in everyItemfor Ark-UI's state machine to function correctly.activationMode— keyboard navigation is always automatic (selection follows focus). This is the correct behaviour for a segmented control per WAI-ARIA.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