Add a new Segmented component (WEB-1382)
Version: 0.10.0 · Type: ✨ Feature
Problem
The design system had no "segmented control" component for switching between a small, mutually-exclusive set of options (e.g. view modes, filter dimensions) with a single always-selected state and no "all unselected" intermediate state. Tabs binds selection to a content panel, which is the wrong shape for a plain value picker.
Changed Files
packages/design/src/components/Segmented/Segmented.tsxpackages/design/src/components/Segmented/styles.tspackages/design/src/components/Segmented/index.tspackages/design/src/components/Segmented/SegmentedDesignSpec.mdpackages/design/src/components/Segmented/__tests__/Segmented.test.tsxpackages/design/src/components/index.tspackages/design-site/docs/components/segmented.mdxpackages/design-site/i18n/zh-CN/docusaurus-plugin-content-docs/current/components/segmented.mdxpackages/design-site/src/theme/ReactLiveScope/index.tsx
Changes
- New
Segmentedcomponent built directly on@radix-ui/react-toggle-group(type="single"), bypassingui/toggle-group— its defaulttoggleVariantsvisual conflicts with Segmented, and reusing it would first require addingunstyledVisualsupport to the toggle family, out of scope for this iteration. - Declarative
optionsAPI with bare-string shorthand ('a'≡{ value: 'a', label: 'a' }), covering three content shapes (text only / icon + text / icon only) viagetSegmentedItemTokenClass, with the icon wrapper centered (inline-flex items-center justify-center) so icons stay vertically aligned with the item and adjacent text. Icon-only options requireiconandaria-labelat the type level (discriminated union), preventing an icon button from shipping without an accessible name. - Supports both controlled (
value) and uncontrolled (defaultValue, falling back to the first option) usage, plusblock(stretch to fill the parent, options split evenly viaflex-1) and whole-control / per-itemdisabled. - Radix's
type="single"firesonValueChangewith an empty string when the already-selected item is clicked (its native "clear selection" behavior); Segmented intercepts the empty value inhandleValueChangeso it never updates state or firesonChange. - Selected state uses
Foregrounds-White/Foregrounds-Blackrather than semantic tokens likeBackgrounds-Primary/Labels-Primary, becauseForegroundsstays pure white/black in both light and dark mode — matching the Figma intent that the selected pill never flips with the theme. - Unselected state text uses
Labels/Secondary(#3D3D3Dlight /#D6D6D6dark) rather than the visually lighterLabels/Tertiary(#6B6B6B):Tertiarymeasures only 4.47:1 (default) / 3.67:1 (hover) against the container background, both below the WCAG 2.1 AA text contrast threshold of 4.5:1, whileSecondarymeasures 9.11:1 (light) / 8.69:1 (dark) default, 7.47:1 (both themes) hover, and the selected state (Foregrounds-BlackonForegrounds-White) measures 21:1 in both themes.
Not Implemented
- antd's
size,shape, andverticalare not implemented — Figma has no corresponding variant for any of them yet. - Container
padding: 2pxandradius: 7pxare hardcoded fallback values; design-tokens has no matching token (onlyRadius_5/Radius_12/Radius_20/Rounded).