Segmented buttons

Segmented buttons help people select options, switch views, or sort elements

Usage

Segmented buttons help people select options, switch views, or sort elements.

Instalation

Run the following command if is not already installed:

npm i @radix-ui/react-checkbox

Copy and paste the following code into your project.

'use client'

import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { cva, type VariantProps } from 'class-variance-authority'

import { createSafeContext } from '@/lib/createSafeContext'
import { cn } from '@/lib/utils'
import { Icon } from '@/components/ui/icon'

const segmentedButtonsGroupVariants = cva('relative flex w-full flex-nowrap', {
  variants: {
    density: {
      default: 'h-10',
      compact: 'h-8',
    },
  },
  defaultVariants: {
    density: 'default',
  },
})

type SegmentedButtonsGroupContextValue = {
  value?: string | string[]
  onValueChange?: (value: string) => void
}

const [SegmentedButtonsGroupProvider, useSegmentedGroup] =
  createSafeContext<SegmentedButtonsGroupContextValue>({
    name: 'SegmentedButtonsGroupContext',
  })

interface SegmentedButtonsGroupProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'defaultValue'>,
    VariantProps<typeof segmentedButtonsGroupVariants> {
  value?: string | string[]
  onValueChange?: React.Dispatch<React.SetStateAction<string | string[]>>
}

const SegmentedButtonsGroupRoot = React.forwardRef<
  HTMLDivElement,
  SegmentedButtonsGroupProps
>(({ className, density, value, onValueChange, ...props }, ref) => {
  const handleSelected = (value: string) => {
    onValueChange &&
      onValueChange((prevSelected) => {
        if (Array.isArray(prevSelected)) {
          if (prevSelected.includes(value)) {
            if (prevSelected.length === 1) return prevSelected
            return prevSelected.filter((v) => v !== value)
          } else {
            return [...(prevSelected || []), value]
          }
        } else {
          return value
        }
      })
  }

  return (
    <SegmentedButtonsGroupProvider
      value={{ value, onValueChange: handleSelected }}
    >
      <div
        ref={ref}
        className={cn(segmentedButtonsGroupVariants({ density, className }))}
        {...props}
      />
    </SegmentedButtonsGroupProvider>
  )
})
SegmentedButtonsGroupRoot.displayName = 'SegmentedButtonsGroupRoot'

interface SegmentedButtonProps
  extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> {
  icon?: React.ReactNode
}

const SegmentedButton = React.forwardRef<
  React.ElementRef<typeof CheckboxPrimitive.Root>,
  SegmentedButtonProps
>(
  (
    { className, id, icon, checked, onCheckedChange, children, ...props },
    ref
  ) => {
    const { value, onValueChange } = useSegmentedGroup()

    const isChecked = (() => {
      if (!id) return false
      if (Array.isArray(value)) return value.includes(id)
      else return value === id
    })()

    return (
      <CheckboxPrimitive.Root
        ref={ref}
        className={cn(
          'group relative z-0 flex h-full w-fit flex-1 select-none items-center justify-center overflow-hidden border border-l-0 px-3 text-label-lg font-medium text-onSurface no-underline outline-none transition first:border-l first-of-type:rounded-l-2xl last-of-type:rounded-r-2xl active:scale-[0.98] disabled:pointer-events-none disabled:border-y-onSurface/38 disabled:text-onSurface/38 disabled:shadow-none first-of-type:disabled:border-l-onSurface/38 last-of-type:disabled:border-r-onSurface/38 data-[state=checked]:bg-secondaryContainer data-[state=checked]:text-onSecondaryContainer',
          className
        )}
        {...(value && {
          checked: isChecked,
        })}
        {...(onValueChange && {
          onCheckedChange: () => id && onValueChange(id),
        })}
        {...props}
      >
        <span
          className={cn(
            'relative h-5 w-5 transition-[width] duration-200',
            !icon
              ? 'group-data-[state=checked]:mr-2 group-data-[state=unchecked]:w-0'
              : 'mr-2'
          )}
        >
          <CheckboxPrimitive.Indicator
            className={cn(
              'absolute inset-0 flex shrink-0 items-center justify-center text-current transition-[transform] duration-200 animate-in data-[state=unchecked]:animate-out data-[state=checked]:zoom-in-0 data-[state=unchecked]:zoom-out-0'
            )}
          >
            <Icon symbol="check" className="text-[20px]" />
          </CheckboxPrimitive.Indicator>
          {icon && (
            <span className="absolute inset-0 flex shrink-0 items-center justify-center text-[18px] text-current transition-[transform] duration-200 animate-in group-data-[state=checked]:hidden group-data-[state=checked]:animate-out group-data-[state=checked]:zoom-out-0 group-data-[state=unchecked]:zoom-in-0 [&>i]:text-[18px] [&>svg]:h-[18px] [&>svg]:w-[18px]">
              {icon}
            </span>
          )}
        </span>
        {children}
        <span className="group-data-[state=checked]:bg-onSecondaryContainer] absolute inset-0 z-[-1] bg-onSurface opacity-0 transition-opacity group-hover:opacity-8 group-focus:opacity-12 group-active:opacity-12" />
      </CheckboxPrimitive.Root>
    )
  }
)
SegmentedButton.displayName = 'SegmentedButton'

const SegmentedButtonsGroup = Object.assign(SegmentedButtonsGroupRoot, {
  Button: SegmentedButton,
})

export {
  SegmentedButtonsGroup,
  SegmentedButton,
  SegmentedButtonsGroupRoot,
  segmentedButtonsGroupVariants,
}

Update the import paths to match your project setup.

Examples

Single select

Use a single-select segmented button to select one option from a set, switch between views, or sort elements from up to five options.

import { useState } from 'react'

import { Icon } from '@/components/ui/icon'
import { SegmentedButtonsGroup } from '@/components/ui/segmented-buttons-group'

export const MultipleSelectSegmentedButtons = () => {
  const [selected, setSelected] = useState<string | string[]>('songs')

  return (
    <SegmentedButtonsGroup
      className="max-w-sm"
      value={selected}
      onValueChange={setSelected}
    >
      <SegmentedButtonsGroup.Button id="songs" icon={<Icon symbol="genres" />}>
        Songs
      </SegmentedButtonsGroup.Button>
      <SegmentedButtonsGroup.Button id="albums" icon={<Icon symbol="album" />}>
        Albums
      </SegmentedButtonsGroup.Button>
      <SegmentedButtonsGroup.Button
        id="podcasts"
        icon={<Icon symbol="podcasts" />}
      >
        Podcasts
      </SegmentedButtonsGroup.Button>
    </SegmentedButtonsGroup>
  )
}

Multiple select

Use a multi-select segmented button to select or sort from two to five options.

import { useState } from 'react'

import { SegmentedButtonsGroup } from '@/components/ui/segmented-buttons-group'

export const MultipleSelectSegmentedButtons = () => {
  // For multiple selection the value have to be an array
  const [selected, setSelected] = useState<string | string[]>(['$'])

  return (
    <SegmentedButtonsGroup
      className="max-w-sm"
      value={selected}
      onValueChange={setSelected}
    >
      <SegmentedButtonsGroup.Button id="$">$</SegmentedButtonsGroup.Button>
      <SegmentedButtonsGroup.Button id="$$">$$</SegmentedButtonsGroup.Button>
      <SegmentedButtonsGroup.Button id="$$$">$$$</SegmentedButtonsGroup.Button>
      <SegmentedButtonsGroup.Button id="$$$$">
        $$$$
      </SegmentedButtonsGroup.Button>
    </SegmentedButtonsGroup>
  )
}