---
title: Toggle
description: A pill-shaped button group where only one option is active at a time, with animated color transitions
---

```tsx
'use client';
import { useState } from 'react';

import { Toggle } from '@/ui/Toggle';

export function ToggleDemo() {
  const [active, setActive] = useState(0);
  const labels = ['Our selection', 'Exclusive Collection', 'Safari'];

  return (
    <div className="space-y-16">
      <Toggle
        aria-label="Choose a collection"
        items={[
          { color: 'saffron', label: 'Our selection', id: 'id1' },
          { color: 'ultramarine', label: 'Exclusive Collection', id: 'id2' },
          { color: 'sienna', label: 'Safari', id: 'id3' },
        ]}
        index={active}
        onClick={setActive}
      />
      <p className="text-b3">Active: {labels[active]}</p>
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/toggle.json
```

Or manually, copy the following code into `/ui/Toggle.tsx`:

```tsx
'use client';
import clsx from 'clsx';
import type { ComponentPropsWithoutRef, CSSProperties, FunctionComponent, ReactNode } from 'react';
import { useEffect, useState } from 'react';

import { getComplementaryTextColor } from './helpers/colors/colors';
import type { Colors } from './types/Colors';

interface Item extends Omit<ComponentPropsWithoutRef<'button'>, 'children'> {
  color: Colors;
  id: string;
  label: ReactNode;
}

export interface ToggleProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'onClick'> {
  index?: number;
  items: Item[];
  onClick?: (index: number) => void;
  size?: 'default' | 'small';
}

export const Toggle: FunctionComponent<ToggleProps> = ({
  className,
  index = 0,
  items = [],
  onClick,
  size = 'default',
  ...attrs
}) => {
  const [active, setActive] = useState(index);

  useEffect(() => {
    setActive(index);
  }, [index]);

  if (!items.length) {
    return null;
  }

  const style = {
    '--toggle-color': `var(--color-${items[active].color || 'black'})`,
  } as CSSProperties;

  return (
    <div
      {...attrs}
      className={clsx(
        `rounded-pill ${size === 'small' ? 'text-b4' : 'text-b3'} border p-4 font-bold`,
        className,
      )}
      role="group"
    >
      <div
        className={clsx(
          'toggle relative isolate flex',
          'before:rounded-pill before:absolute before:z-0 before:bg-(--toggle-color) before:transition-all before:duration-300 before:ease-out',
        )}
        style={style}
      >
        {items.map(({ color, label, ...item }, i) => {
          const isActive = active === i;

          return (
            <button
              {...item}
              aria-pressed={isActive}
              className={clsx(
                item.className,
                `rounded-pill relative cursor-pointer overflow-hidden ${size === 'small' ? 'px-12 py-4' : 'px-20 py-8'} transition-colors`,
                { active: isActive, [getComplementaryTextColor(color)]: isActive },
                'before:absolute before:inset-0 before:-z-1 before:transition-colors hover:before:bg-pearl outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',
              )}
              key={item.id}
              onClick={() => {
                setActive(i);
                onClick?.(i);
              }}
              type="button"
            >
              {label}
            </button>
          );
        })}
      </div>
    </div>
  );
};
```

## Usage

```tsx showLineNumbers
import { Toggle } from '@/ui/Toggle';

export function Component() {
  const [active, setActive] = useState(0);

  return (
    <Toggle
      index={active}
      onClick={setActive}
      items={[
        { color: 'saffron', label: 'Our selection', id: 'selection' },
        { color: 'ultramarine', label: 'Exclusive Collection', id: 'exclusive' },
        { color: 'sienna', label: 'Safari', id: 'safari' },
      ]}
    />
  );
}
```

## Small size

Use `size="small"` for a compact variant suited to inline or space-constrained contexts.

```tsx
'use client';
import { useState } from 'react';

import { Toggle } from '@/ui/Toggle';

export function ToggleSmallDemo() {
  const [active, setActive] = useState(0);
  const labels = ['Year', 'Month'];

  return (
    <div className="space-y-16">
      <Toggle
        aria-label="Choose a period"
        size="small"
        items={[
          { color: 'saffron', label: 'Year', id: 'id1' },
          { color: 'saffron', label: 'Month', id: 'id2' },
        ]}
        index={active}
        onClick={setActive}
      />
      <p className="text-b3">Active: {labels[active]}</p>
    </div>
  );
}
```

## API Reference

### Toggle

| Prop        | Type                      | Default     | Description                                                                                           |
| ----------- | ------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- |
| `items`     | `Item[]`                  | `[]`        | **Required**. Array of items to display as buttons.                                                   |
| `index`     | `number`                  | `0`         | The index of the currently active item.                                                               |
| `onClick`   | `(index: number) => void` | `undefined` | Callback fired with the index of the clicked item.                                                    |
| `size`      | `'default' \| 'small'`   | `'default'` | Controls button padding and text size. `small` uses compact padding for inline use.                   |
| `className` | `string`                  | `undefined` | Additional CSS classes for the outer container.                                                       |

### Item

| Prop        | Type      | Default     | Description                                                            |
| ----------- | --------- | ----------- | ---------------------------------------------------------------------- |
| `id`        | `string`  | —           | **Required**. Unique identifier for the item.                          |
| `color`     | `Colors`  | —           | **Required**. Background color applied when the item is active.        |
| `label`     | `ReactNode` | —         | **Required**. The label rendered inside the button.                    |
| `className` | `string`  | `undefined` | Additional CSS classes for the button.                                 |

All other standard button HTML attributes are also accepted per item.

## Notes

- Renders nothing when `items` is empty
- Each button exposes `aria-pressed` for accessibility
- The active pill background transitions smoothly using a CSS custom property `--toggle-color`
- The `index` prop syncs with an internal `useState`, so the component works both controlled and uncontrolled
