Actions

Toggle Group

A set of pressable toggles that act as one control, built on react-aria-components and styled with the quebi design system. Supports single or multiple selection, horizontal or vertical layout.

actiontogglesegmentedselectioninteractive

Single selection

Exactly one item active at a time — items float with a small gutter.

Multiple selection

Many items active at once — items butt together into a segmented bar.

Sizes

Vertical

Stack the group along the vertical axis.

Pill

Fully rounded with isCircle for a softer, segmented pill.

Controlled

View: grid

Disabled

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { createContext, use } from "react"
import {
  composeRenderProps,
  ToggleButton,
  ToggleButtonGroup,
  type ToggleButtonGroupProps,
  type ToggleButtonProps,
} from "react-aria-components"
import { tv, type VariantProps } from "tailwind-variants"
import { cn } from "@/lib/utils"

/**
 * ToggleGroup — quebi design system
 *
 * A set of two-state pressable buttons that act as one control (think a view
 * switcher or a text-alignment toolbar). Selected items light up with brand
 * teal; in single-selection mode items float with a small gutter, in multiple
 * mode they butt together into a segmented bar. Self-contained — the item
 * styles live here rather than reaching for a sibling Toggle.
 */

type ToggleGroupSize = "xs" | "sm" | "md" | "lg" | "sq-xs" | "sq-sm" | "sq-md" | "sq-lg"

interface ToggleGroupContextValue
  extends Pick<ToggleButtonGroupProps, "selectionMode" | "orientation"> {
  size?: ToggleGroupSize
}

const ToggleGroupContext = createContext<ToggleGroupContextValue>({
  size: "md",
  selectionMode: "single",
  orientation: "horizontal",
})

const useToggleGroupContext = () => use(ToggleGroupContext)

export interface ToggleGroupProps extends ToggleButtonGroupProps {
  size?: ToggleGroupSize
  isCircle?: boolean
}

export function ToggleGroup({
  size = "md",
  orientation = "horizontal",
  selectionMode = "single",
  isCircle,
  className,
  ...props
}: ToggleGroupProps) {
  return (
    <ToggleGroupContext.Provider value={{ size, selectionMode, orientation }}>
      <ToggleButtonGroup
        data-slot="control"
        selectionMode={selectionMode}
        orientation={orientation}
        className={cn(
          "inline-flex p-0.5",
          "border border-solid border-cyan-500/10 bg-quebi-bg/40",
          orientation === "horizontal" ? "flex-row" : "flex-col",
          selectionMode === "single" ? "gap-0.5" : "gap-0",
          isCircle ? "rounded-full" : "rounded-quebi-md",
          className,
        )}
        {...props}
      />
    </ToggleGroupContext.Provider>
  )
}

export const toggleGroupItemStyles = tv({
  base: [
    "inline-flex items-center justify-center gap-2",
    "font-sans font-semibold whitespace-nowrap select-none cursor-pointer",
    "border border-solid border-transparent text-quebi-fg-muted",
    "transition-all duration-200 ease-out",
    "outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-quebi-brand/50 focus-visible:ring-offset-2 focus-visible:ring-offset-quebi-bg focus-visible:z-10",
    "hover:not-selected:bg-white/[0.04] hover:not-selected:text-white",
    "selected:bg-quebi-brand selected:border-quebi-brand selected:text-quebi-bg selected:shadow-quebi-glow selected:hover:bg-quebi-brand-hover selected:hover:border-quebi-brand-hover",
    "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent",
    "*:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center",
  ],
  variants: {
    orientation: {
      horizontal: "justify-center",
      vertical: "justify-start",
    },
    selectionMode: {
      single: "rounded-quebi-sm",
      multiple: "rounded-none",
    },
    size: {
      xs: ["text-xs px-2.5 py-1.5", "*:data-[slot=icon]:size-3.5"],
      sm: ["text-sm px-3 py-2", "*:data-[slot=icon]:size-4"],
      md: ["text-base px-5 py-2.5", "*:data-[slot=icon]:size-5"],
      lg: ["text-lg px-6 py-3", "*:data-[slot=icon]:size-5"],
      "sq-xs": "size-7 p-0 *:data-[slot=icon]:size-3.5",
      "sq-sm": "size-9 p-0 *:data-[slot=icon]:size-4",
      "sq-md": "size-11 p-0 *:data-[slot=icon]:size-5",
      "sq-lg": "size-12 p-0 *:data-[slot=icon]:size-6",
    },
  },
  defaultVariants: {
    size: "md",
    selectionMode: "single",
    orientation: "horizontal",
  },
  compoundVariants: [
    {
      selectionMode: "multiple",
      orientation: "horizontal",
      className: "not-first:-ms-px first:rounded-s-quebi-sm last:rounded-e-quebi-sm",
    },
    {
      selectionMode: "multiple",
      orientation: "vertical",
      className: "not-first:-mt-px first:rounded-t-quebi-sm last:rounded-b-quebi-sm",
    },
  ],
})

export interface ToggleGroupItemProps
  extends ToggleButtonProps,
    Pick<VariantProps<typeof toggleGroupItemStyles>, "size"> {}

export function ToggleGroupItem({ className, size: sizeProp, ...props }: ToggleGroupItemProps) {
  const { size, selectionMode, orientation } = useToggleGroupContext()

  return (
    <ToggleButton
      data-slot="toggle-group-item"
      className={composeRenderProps(className, (className) =>
        cn(
          toggleGroupItemStyles({
            size: sizeProp ?? size,
            orientation,
            selectionMode,
          }),
          className,
        ),
      )}
      {...props}
    />
  )
}