---
title: Toggle
description: A button that stays down, and the single-select group of them - aria-pressed is the whole contract.
source: https://adamjurek.com/components/toggle
---

## Home


A button that stays down: `aria-pressed` and `data-active` mirror the
`pressed` prop, and clicking hands back its opposite - the state itself lives
on the caller. `ToggleGroup` wraps a row of them behind one `value`, for a
single-select set of mutually exclusive options like a device or size picker.

<!-- ::start:showcase demo="toggle" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

`aria-pressed` is the entire contract, because the showcase's own device row
had already been drawing this shape for a while before it got a public name -
Toggle just gives it one. The component holds no state itself; it renders
`pressed` and calls back with its opposite. `ToggleGroup` is single-select
because that's what every use of it here has actually wanted; multi-select is
just several Toggles, each holding its own `pressed`.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/toggle.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/toggle.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Actions |
| Files | `toggle.tsx` |
| Dependencies | None |
| Tags | button, group, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { useState } from "react";
import { Toggle } from "@sushindustries/ui";

export function Example() {
	const [pressed, setPressed] = useState(false);

	return (
		<Toggle pressed={pressed} onPressedChange={setPressed}>
			Bold
		</Toggle>
	);
}
```

## What you should see

A button that looks pressed after a click - a slightly darker fill, a
firmer border - and stays that way until clicked again. `aria-pressed`
flips with it, so a screen reader announces the change as well.

## If nothing happens

`Toggle` has no state of its own - `pressed` is entirely the caller's, so a
click that never updates `pressed` (a missing `onPressedChange`, or one
that does not call `setState`) looks exactly like a broken button: it
fires, and nothing about it changes.


## Guides


## Composing it

`Toggle` holds no state - it renders `pressed` and calls
`onPressedChange(!pressed)` on click, nothing more. Somewhere above it has
to own that boolean, the same as a controlled `<input>`; a `Toggle` with a
`pressed` prop that never changes is a button that looks stuck because it
is stuck.

## Toggle vs ToggleGroup

`Toggle` is one button and one boolean. `ToggleGroup` is a row of them
sharing a single `value`, wrapped in a `<fieldset>` with its own accessible
`label` - reach for it the moment two or more toggles are meant to be
mutually exclusive, rather than composing several `Toggle`s and enforcing
that by hand. Neither gives keyboard users arrow-key movement between
options the way `ThemeToggle`'s radiogroup does; each button in a
`ToggleGroup` is its own tab stop.

## When not to use it

For a larger set of mutually exclusive options where arrow-key navigation
between them matters - a segmented control of five or six choices, say -
`ThemeToggle`'s `radiogroup` pattern is the better fit; `ToggleGroup` is a
row of ordinary buttons, not a roving-focus group.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | What pressing it means. |
| `pressed` | `boolean` | - | Drives `aria-pressed` and `data-active`. The state is the caller's - nothing moves on its own. |
| `onPressedChange` | `(pressed: boolean) => void` | - | Handed the opposite of `pressed`, never the event. |

### ToggleGroupProps

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `options` | `readonly { value: string; label: ReactNode }[]` | - | The choices, keyed by value. |
| `value` | `string` | - | The pressed option. One matching nothing leaves every button up. |
| `onChange` | `(value: string) => void` | - | Handed the chosen value. Pressing the option already down fires it again. |
| `label` | `string` | - | Announced name of the group. |

<!-- /generated:api -->

## Notes

`Toggle` and `ToggleGroup` share no state between them - a standalone
`Toggle` is not aware of any `ToggleGroup` on the same page, so faking a
single-select group out of several `Toggle`s means enforcing mutual
exclusivity by hand. `ToggleGroup` exists so that bookkeeping does not have
to be reinvented per page; reach for it instead of composing `Toggle`s the
moment two options are meant to be exclusive.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="toggle" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { useState } from "react";
import { ToggleGroup } from "@sushindustries/ui";

export function ShowcaseTabs() {
	const [view, setView] = useState("preview");

	return (
		<>
			<ToggleGroup
				label="View"
				value={view}
				onChange={setView}
				options={[
					{ value: "preview", label: "Preview" },
					{ value: "code", label: "Code" },
				]}
			/>
			{view === "preview" ? <p>Rendered output goes here.</p> : <pre>Source goes here.</pre>}
		</>
	);
}
```

## What this example is not

Switching `view` here swaps two static blocks with a plain conditional -
`ToggleGroup` reports the chosen value and stops there. Whatever the
selection controls, including animating between the two states, is code
the page around it owns.
