All components
Segment Group
A segmented control for mutually exclusive views with an inset track and raised active segment.
1234567891011121314import { SegmentGroup } from "#components/ui/ark-ui/segment-group.tsx";
export function Example() { return ( // Root mirrors the selected value so each item can derive its checked // and divider state <SegmentGroup.Root defaultValue="preview" size="md" aria-label="View mode"> <SegmentGroup.Item value="preview">Preview</SegmentGroup.Item> <SegmentGroup.Item value="code">Code</SegmentGroup.Item> </SegmentGroup.Root> );}
// controlled: <SegmentGroup.Root value={view} onValueChange={(d) => d.value && setView(d.value)}>Sizes
xs
sm
md
lg
Install
1npx stylexblocks add segment-groupSource
components/ui/ark-ui/segment-group.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249import * as stylex from "@stylexjs/stylex";import { Children, createContext, isValidElement, useContext, useState } from "react";import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants,} from "#lib/recipe-props.ts";import { colors } from '@stylexblocks/ui/tokens/colors.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { semanticShadows } from '@stylexblocks/ui/semantic-tokens/shadows.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const segmentGroupRoot = stylex.create({ base: { borderRadius: 'var(--segment-radius)', display: 'inline-flex', boxShadow: semanticShadows.inset, minWidth: 'max-content', textAlign: 'center', position: 'relative', isolation: 'isolate', backgroundColor: semanticColors.bgMuted, borderWidth: '1px', },})
const segmentGroupRootSizes = stylex.create({ xs: { '--segment-radius': semanticRadii.control, height: sizes.controlXs, }, sm: { '--segment-radius': semanticRadii.control, height: sizes.controlSm, }, md: { '--segment-radius': semanticRadii.control, height: sizes.controlMd, }, lg: { '--segment-radius': semanticRadii.control, height: sizes.controlLg, },})
const segmentGroupItem = stylex.create({ base: { display: 'flex', alignItems: 'center', userSelect: 'none', fontSize: fontSizes.sm, position: 'relative', color: { default: semanticColors.fgSubtle, ':hover': semanticColors.fg, ':is(:checked, [data-checked], [aria-checked="true"], [data-state="checked"])': semanticColors.fg, }, cursor: cursor.button, borderRadius: 'var(--segment-radius)', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, '::before': { content: '""', position: 'absolute', insetInlineStart: 0, insetBlock: 0, backgroundColor: semanticColors.border, width: '1px', transition: 'opacity 0.2s', }, },})
const segmentGroupItemSizes = stylex.create({ xs: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, paddingInline: spacing._3, gap: spacing._1, }, sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingInline: spacing._4, gap: spacing._2, }, md: { fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._4, gap: spacing._2, }, lg: { fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._5, gap: spacing._3, },})
const segmentGroupIndicator = stylex.create({ base: { position: 'absolute', backgroundColor: `light-dark(${semanticColors.bg}, ${semanticColors.bgEmphasized})`, width: 'var(--width)', height: 'var(--height)', top: 'var(--top)', left: 'var(--left)', zIndex: -1, borderRadius: 'var(--segment-radius)', boxShadow: '0 0 0 1px var(--sui-colors-border-emphasized)', transitionProperty: 'left, top, width, height', transitionDuration: durations.moderate, transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', },})
export type SegmentGroupSize = keyof typeof segmentGroupRootSizes
const segmentGroupRecipe = defineSlotRecipe({ slots: { root: { styles: segmentGroupRoot, sizes: segmentGroupRootSizes }, item: { styles: segmentGroupItem, sizes: segmentGroupItemSizes }, indicator: { styles: segmentGroupIndicator }, }, defaultVariants: { size: "md" },});
const chrome = stylex.create({ hideDivider: { ":is([data-checked], [data-no-divider])::before": { opacity: 0 }, }, cursorPointer: { cursor: "pointer" },});
interface SegmentGroupContextValue { value: string | null; hiddenDividers: ReadonlySet<string>;}
const GroupContext = createContext<SegmentGroupContextValue>({ value: null, hiddenDividers: new Set(),});
function collectHiddenDividers(children: React.ReactNode, selected: string | null) { const values = Children.toArray(children) .map((child) => (isValidElement(child) ? (child.props as { value?: string }).value : undefined)) .filter((value): value is string => typeof value === "string"); const hidden = new Set<string>(); values.forEach((value, index) => { if (index === 0 || value === selected || values[index - 1] === selected) { hidden.add(value); } }); return hidden;}
const { withProvider, withContext } = createSlotRecipeContext(segmentGroupRecipe);
const Indicator = withContext( ArkSegmentGroup.Indicator, "indicator", "SegmentGroupIndicator",);
function RootNode({ value, defaultValue, onValueChange, children, ...props}: React.ComponentProps<typeof ArkSegmentGroup.Root>) { const [inner, setInner] = useState<string | null>(defaultValue ?? null); const current = value !== undefined ? value : inner; return ( <GroupContext.Provider value={{ value: current, hiddenDividers: collectHiddenDividers(children, current) }} > <ArkSegmentGroup.Root {...props} value={current} onValueChange={(details) => { if (details.value) setInner(details.value); onValueChange?.(details); }} > <Indicator /> {children} </ArkSegmentGroup.Root> </GroupContext.Provider> );}
function ItemNode({ value, children, ...props}: React.ComponentProps<typeof ArkSegmentGroup.Item>) { const { value: selected, hiddenDividers } = useContext(GroupContext); const checked = selected === value; return ( <ArkSegmentGroup.Item {...props} value={value} data-checked={checked ? "" : undefined} data-no-divider={hiddenDividers.has(value) ? "" : undefined} > <ArkSegmentGroup.ItemText>{children}</ArkSegmentGroup.ItemText> <ArkSegmentGroup.ItemHiddenInput /> </ArkSegmentGroup.Item> );}
export type SegmentGroupVariantProps = Partial<SlotRecipeVariants<typeof segmentGroupRecipe>>;
/** * Segment Group — Ark UI's segment-group machine styled by the StyleX slot * recipe. `<SegmentGroup.Root size>` resolves the recipe once; every item * inherits it through context. Root mirrors the selected value so items can * stamp `data-checked` / `data-no-divider` from that value and the item order. */export const SegmentGroup = { Root: withProvider(RootNode, "root", "SegmentGroupRoot"), Item: withContext( ItemNode, (v) => [ ...resolveSlotStyles(segmentGroupRecipe.slots.item, v), chrome.hideDivider, chrome.cursorPointer, ], "SegmentGroupItem", ),};
export type SegmentGroupRootProps = React.ComponentProps<typeof SegmentGroup.Root>;export type SegmentGroupItemProps = React.ComponentProps<typeof SegmentGroup.Item>;