ToggleGroup

A set of two-state buttons that can be toggled on or off.

Demo

Dependencies

clsxtailwind-merge

Props

This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().

Source

"use client";

import { useState, useCallback, createContext, useContext } from "react";
import { cn } from "@/lib/utils";

interface ToggleGroupCtxValue {
  value: string | string[];
  onToggle: (v: string) => void;
}

const ToggleGroupCtx = createContext<ToggleGroupCtxValue>({
  value: "",
  onToggle: () => {},
});

export interface ToggleGroupProps {
  type?: "single" | "multiple";
  value?: string | string[];
  defaultValue?: string | string[];
  onValueChange?: (value: string | string[]) => void;
  children?: React.ReactNode;
  className?: string;
}

export function ToggleGroup({
  type = "single",
  value: controlledValue,
  defaultValue,
  onValueChange,
  children,
  className,
}: ToggleGroupProps) {
  const multiple = type === "multiple";
  const [internalValue, setInternalValue] = useState(
    defaultValue ?? (multiple ? [] : ""),
  );
  const isControlled = controlledValue !== undefined;
  const value = isControlled ? controlledValue : internalValue;

  const handleToggle = useCallback(
    (itemValue: string) => {
      let next: string | string[];
      if (multiple) {
        const arr = (Array.isArray(value) ? value : []) as string[];
        next = arr.includes(itemValue)
          ? arr.filter((v) => v !== itemValue)
          : [...arr, itemValue];
      } else {
        next = value === itemValue ? "" : itemValue;
      }
      if (!isControlled) setInternalValue(next);
      onValueChange?.(next);
    },
    [value, multiple, isControlled, onValueChange],
  );

  return (
    <ToggleGroupCtx.Provider value={{ value, onToggle: handleToggle }}>
      <div
        className={cn(
          "inline-flex items-center rounded-md border border-gray-200 bg-gray-100 p-1",
          className,
        )}
      >
        {children}
      </div>
    </ToggleGroupCtx.Provider>
  );
}

export function ToggleGroupItem({
  value: itemValue,
  children,
  className,
}: {
  value: string;
  children?: React.ReactNode;
  className?: string;
}) {
  const ctx = useContext(ToggleGroupCtx);
  const isActive = Array.isArray(ctx.value)
    ? ctx.value.includes(itemValue)
    : ctx.value === itemValue;

  return (
    <button
      type="button"
      onClick={() => ctx.onToggle(itemValue)}
      className={cn(
        "inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
        isActive
          ? "bg-white text-gray-900 shadow-sm"
          : "text-gray-500 hover:text-gray-700",
        className,
      )}
    >
      {children}
    </button>
  );
}

CLI

npx genui add toggle-group