# Toggle button

> A button that remembers its pressed state. Reach for it when the same control must flip between "on" and "off" without leaving the row.

- Category: form
- Status: stable (since 0.1.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/button/
- Tokens: --cta-background, --cta-foreground, --foreground-primary, --background-secondary
- Playground: https://design.freecodecamp.org/playground#toggle-button
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `ToggleButton.tsx` → `src/ui/toggle-button/ToggleButton.tsx` (raw: https://design.freecodecamp.org/registry/toggle-button/ToggleButton.tsx)
  - `toggle-button.css` → `src/ui/toggle-button/toggle-button.css` (raw: https://design.freecodecamp.org/registry/toggle-button/toggle-button.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/toggle-button/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/toggle-button/toggle-button.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

`<ToggleButton>` is a pressable button that surfaces its state through
`aria-pressed`. Use it for icon-bar toolbars (bold / italic / strike)
and settings where the label never changes but the meaning flips.

## Keyboard

| Key           | Action                                     |
| ------------- | ------------------------------------------ |
| Space / Enter | Toggles the pressed state.                 |
| Tab           | Moves focus to the next focusable element. |

## Accessibility

Renders a native `<button>` with `aria-pressed="true|false"`. Screen
readers announce the state as "toggle button, pressed" /
"…not pressed". Label text should describe the action, not the state.

## Example

```tsx
import { useState } from 'react';
import { ToggleButton } from './ui/toggle-button/ToggleButton';
export function Pressed() {
  const [on, setOn] = useState(false);
  return (
    <ToggleButton pressed={on} onPressedChange={setOn}>
      {on ? 'On' : 'Off'}
    </ToggleButton>
  );
}
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `pressed` | `boolean` | no | - |  |
| `defaultPressed` | `boolean` | no | - |  |
| `onPressedChange` | `((pressed: boolean) => void)` | no | - |  |
| `size` | `enum` | no | `md` |  |

## Source: ToggleButton.tsx

```tsx
import React, { forwardRef, useState } from 'react';

export type ToggleButtonSize = 'sm' | 'md' | 'lg';

export interface ToggleButtonProps extends Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  'onChange'
> {
  pressed?: boolean;
  defaultPressed?: boolean;
  onPressedChange?: (pressed: boolean) => void;
  size?: ToggleButtonSize;
}

export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
  (
    {
      pressed,
      defaultPressed,
      onPressedChange,
      size = 'md',
      className = '',
      onClick,
      children,
      ...rest
    },
    ref
  ) => {
    const [internal, setInternal] = useState(defaultPressed ?? false);
    const isControlled = pressed !== undefined;
    const value = isControlled ? pressed : internal;

    const classes = [
      'toggle-btn',
      size !== 'md' && `toggle-btn--${size}`,
      className
    ]
      .filter(Boolean)
      .join(' ');

    const handleClick: React.MouseEventHandler<HTMLButtonElement> = e => {
      const next = !value;
      if (!isControlled) setInternal(next);
      onPressedChange?.(next);
      onClick?.(e);
    };

    return (
      <button
        ref={ref}
        type='button'
        aria-pressed={value}
        className={classes}
        onClick={handleClick}
        {...rest}
      >
        {children}
      </button>
    );
  }
);
ToggleButton.displayName = 'ToggleButton';
```

## Source: toggle-button.css

```css
.toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  padding: 6px 18px;
  border: var(--border-width-thick) solid var(--border-strong);
  box-shadow: inset 0 1px 0 var(--surface-elevation-2);
  background: var(--background-tertiary);
  color: var(--foreground-secondary);
  cursor: pointer;
  transition:
    background-color 120ms,
    color 120ms,
    border-color 120ms;
}
.toggle-btn[aria-pressed='true'] {
  background: var(--foreground-primary);
  color: var(--background-primary);
  border-color: var(--foreground-primary);
}
.toggle-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.toggle-btn--sm {
  padding: 4px 14px;
  font-size: var(--fs-sm);
}
.toggle-btn--lg {
  padding: 10px 22px;
  font-size: var(--fs-lg);
}
```

## HTML / vanilla variant

```html
<button class="toggle-btn" aria-pressed="false">Off</button>
<button class="toggle-btn" aria-pressed="true">On</button>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
