---
title: "切换按钮"
description: "Toggle 是带按下状态的按钮：单独用于收藏、置顶，或组成共享值、方向键导航的切换按钮组。"
---

> Documentation Index
> Fetch the complete documentation index at: https://ui-sdk.ndjp.net/llms.txt
> Use this file to discover all available pages before exploring further.

# 切换按钮

`Toggle`（切换按钮）渲染一个原生按钮，用 `aria-pressed` 报告按下状态：按一下保持按下，再按一下弹起，适合收藏文档、开启加粗这类操作。把多个按钮放进 `ToggleGroup` 可以共享一个值、用方向键在按钮间移动焦点，例如编辑器的格式栏或视图切换器。需要「方向键移动即选中」的单选时用[单选框](https://ui-sdk.ndjp.net/docs/components/radio)。

## 演示

点击切换按钮，或聚焦后按空格、回车，下方的状态文字随之更新：

## 组合

部件都从 `@ui-sdk/react/toggle` 导入。单独使用时就是 `Toggle` 加可选的 `ToggleIcon`；分组时：

```text
ToggleGroup
├── Toggle
│   └── ToggleIcon
└── Toggle
    └── ToggleIcon
```

`ToggleIcon` 可选，承载图标并对屏幕阅读器隐藏。组内每个 `Toggle` 都要给 `value`，组的值就是被按下按钮的 `value` 组成的数组。

## 用法

```tsx
import { Toggle, ToggleIcon } from "@ui-sdk/react/toggle";

function Example() {
  return (
    <Toggle defaultPressed>
      <ToggleIcon>★</ToggleIcon>
      收藏
    </Toggle>
  );
}
```

两种状态下文字保持一致：屏幕阅读器会读出按钮是否按下，不必把标签改成「已收藏」。只有图标没有文字时必须给 `aria-label`。按钮始终是 `type="button"`，在表单里按下它不会触发提交。

## 分组与单选

`multiple` 允许多个按钮同时按下，方向键只移动焦点、不改变按下状态：

```tsx
import { Toggle, ToggleGroup } from "@ui-sdk/react/toggle";

<ToggleGroup multiple defaultValue={["bold"]} aria-label="文本格式">
  <Toggle value="bold">加粗</Toggle>
  <Toggle value="italic">斜体</Toggle>
  <Toggle value="underline">下划线</Toggle>
</ToggleGroup>;
```

不传 `multiple` 时同一时刻只有一个按下：按新按钮会弹起其他按钮，再按当前按下的按钮会清空选择。设 `deselectable={false}` 让选择始终保留一项，并搭配 `defaultValue` 指定初始项，例如视图切换器用 `defaultValue={["list"]}` 保证总有一种视图被选中。

组的值**始终是数组**，按按钮在文档里的顺序排列；组内按钮自己的 `pressed` / `defaultPressed` 不生效。有方向含义的图标（箭头、对齐）给 `ToggleIcon` 加 `mirror`，在从右到左的文本里水平翻转；时钟、对勾这类描绘事物的图标不用翻。

## 禁用与可聚焦

`disabled` 可以设在按钮或整个组上，禁用的按钮仍能以按下状态开始（`<Toggle disabled defaultPressed>`），外层禁用的 `<fieldset>` 也会禁用里面的按钮。

单独的禁用按钮会离开 Tab 顺序；组或工具栏里的禁用按钮仍能被方向键到达，报告 `aria-disabled` 但忽略按下。给单独的按钮加 `focusableWhenDisabled` 可以保持可聚焦，并用 `aria-describedby` 解释原因：

```tsx
<Toggle disabled focusableWhenDisabled aria-describedby="pin-hint">
  置顶
</Toggle>
<p id="pin-hint">只有所有者可以置顶消息。</p>
```

## 受控与控制器

单独的按钮用 `pressed` + `onPressedChange`，回调里调用 `details.cancel()` 保持现状。`details.reason` 为 `"press"`（用户按下）或 `"imperative"`（控制器调用）：

```tsx
"use client";

import * as React from "react";
import { createToggleController, Toggle } from "@ui-sdk/react/toggle";

function StarButton() {
  const [starred, setStarred] = React.useState(false);
  const [star] = React.useState(createToggleController);
  return (
    <>
      <Toggle pressed={starred} onPressedChange={setStarred} controller={star}>
        收藏
      </Toggle>
      <button type="button" onClick={() => star.toggle()}>快捷键收藏</button>
    </>
  );
}
```

每个挂载的按钮配一个 `createToggleController()`：`isPressed / setPressed / toggle / focus`。写方法接受可选的 `{ reason, event }`，像用户按下一样触发回调，被取消时返回 `false`。

组用 `value` + `onValueChange`（数组），挂载时不触发。按钮自己的 `onPressedChange` 先于组的回调运行，在任一处 `details.cancel()` 都会取消这次变更。`createToggleGroupController()` 提供 `getValue / setValue / isPressed(value) / toggle(value) / focus(value?)`，`setValue([])` 清空选择；非 `multiple` 时数组里只有第一个值生效。参见[状态与事件](https://ui-sdk.ndjp.net/docs/concepts/state-and-events)与[控制器](https://ui-sdk.ndjp.net/docs/concepts/controllers)。

## 样式与状态

- 按钮 `toggle`：`data-pressed`（按下）、`data-disabled`、`data-orientation`（组的方向）、`data-has-icon`（含图标部件）。
- 图标 `toggle-icon`：`data-mirror`（设了 `mirror`）。
- 组 `toggle-group`：`data-orientation`（`horizontal` / `vertical`）、`data-disabled`、`data-has-icons`（任一按钮含图标，方便没有图标的按钮对齐）。

```css
[data-slot="toggle"] {
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-card);
}
[data-slot="toggle"][data-pressed] {
  border-color: var(--color-brand);
  background: var(--color-brand-wash);
  color: var(--color-brand);
}
[data-slot="toggle"][data-disabled] {
  opacity: 0.5;
}
[data-slot="toggle-group"] {
  display: inline-flex;
  gap: 0.25rem;
}
[data-slot="toggle-group"][data-orientation="vertical"] {
  flex-direction: column;
}
```

`orientation="vertical"` 让组改用上下键移动；`loop={false}` 让方向键停在两端而不循环。每个部件都接受 `render`，可以按 `state.pressed` 渲染实心或空心图标，但要把部件的 props 留在按钮上，`aria-pressed` 与键盘行为才不会断。详见[样式与动画](https://ui-sdk.ndjp.net/docs/concepts/styling-and-animation)与[部件与组合](https://ui-sdk.ndjp.net/docs/concepts/parts-and-composition)。

## API

`Toggle` 常用属性：

| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `pressed` / `defaultPressed` | `boolean` | 受控 / 初始按下状态，组内由组的值决定 |
| `onPressedChange` | `(pressed, details) => void` | 按下回调，`details.cancel()` 可取消 |
| `value` | `string` | 组内标识，默认为生成的 id |
| `disabled` | `boolean` | 禁止按下 |
| `focusableWhenDisabled` | `boolean` | 禁用时保持可聚焦，改用 `aria-disabled` |
| `controller` | `object` | `createToggleController()` 创建 |

`ToggleGroup` 常用属性：

| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `value` / `defaultValue` | `string[]` | 受控 / 初始按下值，始终是数组 |
| `onValueChange` | `(value, details) => void` | 值变更回调，挂载时不触发 |
| `multiple` | `boolean` | 允许多个同时按下 |
| `deselectable` | `boolean` | 单选时能否再按取消，`multiple` 下忽略 |
| `orientation` | `"horizontal" \| "vertical"` | 方向键的移动方向 |
| `loop` | `boolean` | 方向键是否首尾循环，默认循环 |
| `disabled` | `boolean` | 禁用整组 |

`ToggleIcon` 只有 `mirror`（`boolean`）。切换按钮不提交表单值：表单里的选项用[复选框](https://ui-sdk.ndjp.net/docs/components/checkbox)或[单选框](https://ui-sdk.ndjp.net/docs/components/radio)；确要把组留在表单里时，自行把值复制进隐藏输入。

## 键盘与无障碍

按钮遵循 WAI-ARIA 的切换按钮模式：原生 button 加 `aria-pressed`。组借用工具栏的键盘模型，但角色是 `group` 而不是 `toolbar`，因此可以嵌进工具栏；也不是 `radiogroup`，因为成员是按钮。组只有一个 Tab 停靠点：首次 Tab 进入第一个启用的按下按钮（没有则第一个启用按钮），之后回到上次聚焦的按钮。

| 按键 | 行为 |
| --- | --- |
| `Enter` / `Space` | 按下或弹起聚焦的按钮 |
| `Tab` | 进入或离开组 |
| `ArrowLeft` / `ArrowRight` | 水平组内移动焦点，RTL 下反向，不改变按下状态 |
| `ArrowUp` / `ArrowDown` | 垂直组内移动焦点 |
| `Home` / `End` | 聚焦第一个 / 最后一个按钮 |

## 相关组件

- [开关](https://ui-sdk.ndjp.net/docs/components/switch)：改动立即生效的设置，角色是 `switch`，随表单提交值。
- [单选框](https://ui-sdk.ndjp.net/docs/components/radio)：方向键移动焦点的同时即选中。
- [复选框](https://ui-sdk.ndjp.net/docs/components/checkbox)：需要随表单提交的勾选项。

Source: https://ui-sdk.ndjp.net/docs/components/toggle/index.mdx
