---
title: Toggle Group
description: A set of related toggles for one or more values.
group: Components
order: 70
---

Group toggles that belong together. Use `type="single"` for one value or `type="multiple"` when several can remain on.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup type="multiple">
      <ToggleGroupItem value="bold" aria-label="Toggle bold">
        <BoldIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Toggle italic">
        <ItalicIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Toggle underline">
        <UnderlineIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add toggle-group
```

## Usage

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
```

```tsx
<ToggleGroup type="single">
  <ToggleGroupItem value="a">A</ToggleGroupItem>
  <ToggleGroupItem value="b">B</ToggleGroupItem>
  <ToggleGroupItem value="c">C</ToggleGroupItem>
</ToggleGroup>
```

## Composition

```tree
ToggleGroup
├── ToggleGroupItem
└── ToggleGroupItem
```

## Outline

Set `variant="outline"` on the group to give every item a border.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup type="single" variant="outline" defaultValue="all">
      <ToggleGroupItem value="all">All</ToggleGroupItem>
      <ToggleGroupItem value="missed">Missed</ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## Size

Set `size` on the group to `sm`, default, or `lg`; its items follow automatically.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <div className="flex flex-col items-center gap-4">
      <ToggleGroup type="single" size="sm" defaultValue="left">
        <ToggleGroupItem value="top" aria-label="Align top">
          <AlignTopIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="bottom" aria-label="Align bottom">
          <AlignBottomIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="left" aria-label="Align left">
          <AlignLeftIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="right" aria-label="Align right">
          <AlignRightIcon />
        </ToggleGroupItem>
      </ToggleGroup>
      <ToggleGroup type="single" defaultValue="left">
        <ToggleGroupItem value="top" aria-label="Align top">
          <AlignTopIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="bottom" aria-label="Align bottom">
          <AlignBottomIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="left" aria-label="Align left">
          <AlignLeftIcon />
        </ToggleGroupItem>
        <ToggleGroupItem value="right" aria-label="Align right">
          <AlignRightIcon />
        </ToggleGroupItem>
      </ToggleGroup>
    </div>
  )
}
```

## Spacing

Items start with a `2` gap. Pass `spacing={0}` to join them into one control; outline joins use the same edge treatment as Button Group.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup
      type="single"
      variant="outline"
      spacing={0}
      defaultValue="left"
    >
      <ToggleGroupItem value="top" aria-label="Align top">
        <AlignTopIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="bottom" aria-label="Align bottom">
        <AlignBottomIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="left" aria-label="Align left">
        <AlignLeftIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="right" aria-label="Align right">
        <AlignRightIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## Vertical

Stack the row with `orientation="vertical"`.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup type="multiple" orientation="vertical">
      <ToggleGroupItem value="bold" aria-label="Toggle bold">
        <BoldIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Toggle italic">
        <ItalicIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Toggle underline">
        <UnderlineIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## Disabled

`disabled` on the group locks all items at once.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup type="multiple" disabled>
      <ToggleGroupItem value="bold" aria-label="Toggle bold">
        <BoldIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="italic" aria-label="Toggle italic">
        <ItalicIcon />
      </ToggleGroupItem>
      <ToggleGroupItem value="underline" aria-label="Toggle underline">
        <UnderlineIcon />
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## Custom

Style individual items for labels, weights, or other one-off treatments. Joined outlines still follow `spacing={0}`.

```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

export function Example() {
  return (
    <ToggleGroup
      type="single"
      variant="outline"
      spacing={0}
      defaultValue="normal"
      className="flex-wrap"
    >
      <ToggleGroupItem value="light" className="px-3 font-light">
        Aa Light
      </ToggleGroupItem>
      <ToggleGroupItem value="normal" className="px-3 font-normal">
        Aa Normal
      </ToggleGroupItem>
      <ToggleGroupItem value="medium" className="px-3 font-medium">
        Aa Medium
      </ToggleGroupItem>
      <ToggleGroupItem value="bold" className="px-3 font-bold">
        Aa Bold
      </ToggleGroupItem>
    </ToggleGroup>
  )
}
```

## API Reference

### ToggleGroup

| Prop | Type | Default |
| --- | --- | --- |
| type | "single" \| "multiple" | "single" |
| value | string \| string[] | — |
| defaultValue | string \| string[] | — |
| onValueChange | (value: string \| string[]) => void | — |
| variant | "default" \| "outline" | "default" |
| size | "default" \| "sm" \| "lg" | "default" |
| spacing | number | 2 |
| orientation | "horizontal" \| "vertical" | "horizontal" |
| disabled | boolean | false |
| className | string | — |

`value` / `defaultValue` / `onValueChange` use a string when `type="single"`, and a string array when `type="multiple"`.

```tsx
<ToggleGroup type="single" defaultValue="center">
  <ToggleGroupItem value="left">Left</ToggleGroupItem>
  <ToggleGroupItem value="center">Center</ToggleGroupItem>
</ToggleGroup>
```

### ToggleGroupItem

| Prop | Type | Default |
| --- | --- | --- |
| value | string | — |
| variant | "default" \| "outline" | — |
| size | "default" \| "sm" \| "lg" | — |
| disabled | boolean | false |
| className | string | — |

Omit `variant` or `size` on an item to inherit from the group.

```tsx
<ToggleGroupItem value="left">Left</ToggleGroupItem>
```

## Source

```tsx
"use client";

import {
  Children,
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
  type ButtonHTMLAttributes,
  type CSSProperties,
  type HTMLAttributes,
  type MouseEvent,
  type ReactNode,
} from "react";
import {
  buttonGroupItemClass,
  buttonGroupOutlineJoinClass,
} from "@/components/ui/button-group";
import {
  toggleVariants,
  type ToggleSize,
  type ToggleVariant,
} from "@/components/ui/toggle";
import { cn } from "@/lib/utils";

type Orientation = "horizontal" | "vertical";
type Slot = "single" | "first" | "middle" | "last";

type ToggleGroupContextValue = {
  type: "single" | "multiple";
  value: string[];
  toggle: (itemValue: string) => void;
  variant: ToggleVariant;
  size: ToggleSize;
  spacing: number;
  orientation: Orientation;
  disabled: boolean;
  slot: Slot;
  overlap: boolean;
};

const ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null);

function useToggleGroup() {
  const context = useContext(ToggleGroupContext);
  if (!context) {
    throw new Error("ToggleGroupItem must be used within <ToggleGroup>");
  }
  return context;
}

function toArray(value: string | string[] | undefined): string[] {
  if (value === undefined) return [];
  return Array.isArray(value) ? value : value === "" ? [] : [value];
}

function slotFor(index: number, total: number): Slot {
  if (total <= 1) return "single";
  if (index === 0) return "first";
  if (index === total - 1) return "last";
  return "middle";
}

type ToggleGroupBaseProps = Omit<
  HTMLAttributes<HTMLDivElement>,
  "defaultValue" | "dir"
> & {
  variant?: ToggleVariant;
  size?: ToggleSize;
  spacing?: number;
  orientation?: Orientation;
  disabled?: boolean;
  children?: ReactNode;
};

type ToggleGroupSingleProps = ToggleGroupBaseProps & {
  type?: "single";
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
};

type ToggleGroupMultipleProps = ToggleGroupBaseProps & {
  type: "multiple";
  value?: string[];
  defaultValue?: string[];
  onValueChange?: (value: string[]) => void;
};

type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;

export function ToggleGroup({
  className,
  variant = "default",
  size = "default",
  spacing = 2,
  orientation = "horizontal",
  disabled = false,
  children,
  type = "single",
  value: valueProp,
  defaultValue,
  onValueChange,
  style,
  ...props
}: ToggleGroupProps) {
  const controlled = valueProp !== undefined;
  const [uncontrolled, setUncontrolled] = useState(() =>
    toArray(defaultValue),
  );
  const value = controlled ? toArray(valueProp) : uncontrolled;
  const items = Children.toArray(children).filter(Boolean);
  const connected = spacing === 0;

  const toggle = useCallback(
    (itemValue: string) => {
      if (disabled) return;

      let next: string[];
      if (type === "multiple") {
        next = value.includes(itemValue)
          ? value.filter((item) => item !== itemValue)
          : [...value, itemValue];
      } else {
        next = value.includes(itemValue) ? [] : [itemValue];
      }

      if (!controlled) setUncontrolled(next);

      if (type === "multiple") {
        (onValueChange as ((value: string[]) => void) | undefined)?.(next);
      } else {
        (onValueChange as ((value: string) => void) | undefined)?.(
          next[0] ?? "",
        );
      }
    },
    [controlled, disabled, onValueChange, type, value],
  );

  const base = useMemo(
    () => ({
      type,
      value,
      toggle,
      variant,
      size,
      spacing,
      orientation,
      disabled,
    }),
    [type, value, toggle, variant, size, spacing, orientation, disabled],
  );

  return (
    <div
      role="group"
      data-slot="toggle-group"
      data-variant={variant}
      data-size={size}
      data-spacing={spacing}
      data-orientation={orientation}
      style={
        {
          ...style,
          gap: `${spacing * 0.25}rem`,
        } as CSSProperties
      }
      className={cn(
        "flex w-fit items-center",
        orientation === "vertical" ? "flex-col" : "flex-row",
        className,
      )}
      {...props}
    >
      {items.map((child, index) => {
        const slot = connected ? slotFor(index, items.length) : "single";
        const overlap = connected && index > 0;

        return (
          <ToggleGroupContext.Provider
            key={index}
            value={{ ...base, slot, overlap }}
          >
            {child}
          </ToggleGroupContext.Provider>
        );
      })}
    </div>
  );
}

type ToggleGroupItemProps = Omit<
  ButtonHTMLAttributes<HTMLButtonElement>,
  "value" | "children"
> & {
  value: string;
  variant?: ToggleVariant;
  size?: ToggleSize;
  children?: ReactNode;
};

export function ToggleGroupItem({
  className,
  children,
  value,
  variant: variantProp,
  size: sizeProp,
  disabled: disabledProp,
  type = "button",
  onClick,
  ...props
}: ToggleGroupItemProps) {
  const group = useToggleGroup();
  const variant = variantProp ?? group.variant;
  const size = sizeProp ?? group.size;
  const disabled = disabledProp || group.disabled;
  const pressed = group.value.includes(value);
  const connected = group.spacing === 0;
  const joinGroup = connected
    ? {
        orientation: group.orientation,
        slot: group.slot,
        overlap: group.overlap,
      }
    : null;
  const outlineJoinStay =
    connected &&
    group.overlap &&
    variant === "outline" &&
    (group.orientation === "horizontal"
      ? "data-[state=on]:z-10 data-[state=on]:!border-l-border"
      : "data-[state=on]:z-10 data-[state=on]:!border-t-border");

  function handleClick(event: MouseEvent<HTMLButtonElement>) {
    onClick?.(event);
    if (event.defaultPrevented || disabled) return;
    group.toggle(value);
  }

  return (
    <button
      type={type}
      data-slot="toggle-group-item"
      data-variant={variant}
      data-size={size}
      data-spacing={group.spacing}
      data-orientation={group.orientation}
      data-state={pressed ? "on" : "off"}
      aria-pressed={pressed}
      disabled={disabled}
      className={cn(
        toggleVariants({ variant, size }),
        "min-w-0 shrink-0 px-3",
        buttonGroupItemClass(joinGroup),
        variant === "outline" && buttonGroupOutlineJoinClass(joinGroup),
        outlineJoinStay,
        className,
      )}
      onClick={handleClick}
      {...props}
    >
      {children}
    </button>
  );
}
```
