CoreUI is in beta - APIs and components may change.
CoreUI
Base Components

Segmented Control

Segmented control is used to pick one choice from a linear set of closely related choices, and immediately apply that selection.


Installation

Install the following dependencies:

npm install @radix-ui/react-tabs @radix-ui/react-slot merge-refs

Create a use-tab-observer.ts file and paste the following code into it.

hooks/use-tab-observer.ts
// CoreUI useTabObserver v0.0.0import * 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();    };  }, [handleUpdate]);  return { mounted, listRef };}

Create a segmented-control.tsx file and paste the following code into it.

components/ui/segmented-control.tsx
// CoreUI 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 { cn } 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={cn(        '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={cn(          '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={cn(        // 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,};

Examples

Rounded

API Reference

This component is based on the Radix UI Tabs primitives. Refer to their documentation for the API reference.

On this page