Base Components
Segmented control is used to pick one choice from a linear set of closely related choices, and immediately apply that selection.
npm install @radix-ui/react-tabs
use-tab-observer.ts
// AlignUI useTabObserver v0.0.0 import * as React from 'react'; interface TabObserverOptions { onActiveTabChange?: (index: number, element: HTMLElement) => void; } export function useTabObserver({ onActiveTabChange }: TabObserverOptions = {}) { const [mounted, setMounted] = React.useState(false); const listRef = React.useRef<HTMLDivElement>(null); const onActiveTabChangeRef = React.useRef(onActiveTabChange); React.useEffect(() => { onActiveTabChangeRef.current = onActiveTabChange; }, [onActiveTabChange]); const handleUpdate = React.useCallback(() => { if (listRef.current) { const tabs = listRef.current.querySelectorAll('[role="tab"]'); tabs.forEach((el, i) => { if (el.getAttribute('data-state') === 'active') { onActiveTabChangeRef.current?.(i, el as HTMLElement); } }); } }, []); React.useEffect(() => { setMounted(true); const resizeObserver = new ResizeObserver(handleUpdate); const mutationObserver = new MutationObserver(handleUpdate); if (listRef.current) { resizeObserver.observe(listRef.current); mutationObserver.observe(listRef.current, { childList: true, subtree: true, attributes: true, }); } handleUpdate(); return () => { resizeObserver.disconnect(); mutationObserver.disconnect(); }; }, []); return { mounted, listRef }; }
segmented-control.tsx
// AlignUI SegmentedControl v0.0.0 'use client'; import * as React from 'react'; import { Slottable } from '@radix-ui/react-slot'; import * as TabsPrimitive from '@radix-ui/react-tabs'; import mergeRefs from 'merge-refs'; import { useTabObserver } from '@/hooks/use-tab-observer'; import { cnExt } from '@/utils/cn'; const SegmentedControlRoot = TabsPrimitive.Root; SegmentedControlRoot.displayName = 'SegmentedControlRoot'; const SegmentedControlList = React.forwardRef< React.ComponentRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & { floatingBgClassName?: string; } >(({ children, className, floatingBgClassName, ...rest }, forwardedRef) => { const [lineStyle, setLineStyle] = React.useState({ width: 0, left: 0 }); const { mounted, listRef } = useTabObserver({ onActiveTabChange: (_, activeTab) => { const { offsetWidth: width, offsetLeft: left } = activeTab; setLineStyle({ width, left }); }, }); return ( <TabsPrimitive.List ref={mergeRefs(forwardedRef, listRef)} className={cnExt( 'relative isolate grid w-full auto-cols-fr grid-flow-col gap-1 rounded-10 bg-bg-weak-50 p-1', className, )} {...rest} > <Slottable>{children}</Slottable> {/* floating bg */} <div className={cnExt( 'absolute inset-y-1 left-0 -z-10 rounded-md bg-bg-white-0 shadow-toggle-switch transition-transform duration-300', { hidden: !mounted, }, floatingBgClassName, )} style={{ transform: `translate3d(${lineStyle.left}px, 0, 0)`, width: `${lineStyle.width}px`, transitionTimingFunction: 'cubic-bezier(0.65, 0, 0.35, 1)', }} aria-hidden='true' /> </TabsPrimitive.List> ); }); SegmentedControlList.displayName = 'SegmentedControlList'; const SegmentedControlTrigger = React.forwardRef< React.ComponentRef<typeof TabsPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> >(({ className, ...rest }, forwardedRef) => { return ( <TabsPrimitive.Trigger ref={forwardedRef} className={cnExt( // base 'peer', 'relative z-10 h-7 whitespace-nowrap rounded-md px-1 text-label-sm text-text-soft-400 outline-none', 'flex items-center justify-center gap-1.5', 'transition duration-300 ease-out', // focus 'focus:outline-none', // active 'data-[state=active]:text-text-strong-950', className, )} {...rest} /> ); }); SegmentedControlTrigger.displayName = 'SegmentedControlTrigger'; const SegmentedControlContent = React.forwardRef< React.ComponentRef<typeof TabsPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> >(({ ...rest }, forwardedRef) => { return <TabsPrimitive.Content ref={forwardedRef} {...rest} />; }); SegmentedControlContent.displayName = 'SegmentedControlContent'; export { SegmentedControlRoot as Root, SegmentedControlList as List, SegmentedControlTrigger as Trigger, SegmentedControlContent as Content, };
This component is based on the Radix UI Tabs primitives. Refer to their documentation for the API reference.