---
title: 按钮 Btn
url: https://ui.zhaozimin.cn/components/button/
markdown: https://ui.zhaozimin.cn/components/button.md
group: 组件
origin: 设计系统源码（src/system），本仓库是唯一源头
source:
  - src/system/components/v4/Button.jsx  # https://ui.zhaozimin.cn/src/system/components/v4/Button.jsx
  - src/system/styles/v4/controls.css  # https://ui.zhaozimin.cn/src/system/styles/v4/controls.css
version: v0.2.0  # 设计系统版本，发布于 2026-09-23
scope: .zzm-v4   # 根节点必须带这个类，否则样式不生效
css: https://ui.zhaozimin.cn/v/0.2.0/zzm.css   # 锁版本地址，本仓库再改也不影响你
tokens: [--zzm-accent]
---

# 按钮 Btn

> 光从正上方来——每个按钮都是一枚真实的键：凸面渐变、顶部反光、贴地投影、反光光晕、按压内陷。文字按钮一律圆角矩形，禁胶囊。

## 试验台

三条轴自由组合：五种键面 × 三档高度 × 三种状态。下方同时给出对应的 JSX 和类名。

### 样张 · ButtonPlayground

（交互试验台，仅在网页上可操作；各组合的代码见下方样张）

## 五种键面

**纸键**是默认；**墨键**给一屏里最主要的操作；**朱键**只给品牌强调、播放或破坏性确认；**幽灵键**静默时是文字，悬停才「长成键」，用于工具条；**暗面键**只在深色面上用。

### 样张 · 纸 · 墨 · 朱 · 幽灵

React：

```jsx
import { Btn, btnClass } from './components/v4/Button.jsx'

export function Variants() {
  return (
    <>
      <Btn>纸键（默认）</Btn>
      <Btn variant="primary">墨键 · 主操作</Btn>
      <Btn variant="accent">朱键 · 强调</Btn>
      <Btn variant="ghost">幽灵键</Btn>
    </>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<button type="button" class="zzm-btn">纸键（默认）</button>
<button type="button" class="zzm-btn zzm-btn--primary">墨键 · 主操作</button>
<button type="button" class="zzm-btn zzm-btn--accent">朱键 · 强调</button>
<button type="button" class="zzm-btn zzm-btn--ghost">幽灵键</button>
```

### 样张 · 暗面键

付款页 / 播放器锁罩 / 海报态

React：

```jsx
import { Btn, btnClass } from './components/v4/Button.jsx'

export function OnDark() {
  return (
    <>
      <Btn variant="ondark">暗面键</Btn>
      <Btn variant="accent">朱键在暗面同样成立</Btn>
      <Btn variant="ondark" disabled>禁用</Btn>
    </>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<button type="button" class="zzm-btn zzm-btn--ondark">暗面键</button>
<button type="button" class="zzm-btn zzm-btn--accent">朱键在暗面同样成立</button>
<button type="button" disabled="" class="zzm-btn zzm-btn--ondark">禁用</button>
```

## 三档高度

高度落在 4pt 网格上：`sm` 28（工具条）、`md` 36（表单与一般操作）、`lg` 44（主操作，圆角升到 12）。`icon` 把键变成正方形。

### 样张 · 28 · 36 · 44 · 图标键

React：

```jsx
import { Btn, btnClass } from './components/v4/Button.jsx'

export function Sizes() {
  return (
    <>
      <Btn size="sm">小 · 28</Btn>
      <Btn>中 · 36</Btn>
      <Btn size="lg" variant="primary">大 · 44 · 圆角 12</Btn>
      <Btn icon title="图标键">✦</Btn>
      <Btn icon size="sm" variant="ghost" title="小图标键">‹</Btn>
    </>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<button type="button" class="zzm-btn zzm-btn--sm">小 · 28</button>
<button type="button" class="zzm-btn">中 · 36</button>
<button type="button" class="zzm-btn zzm-btn--primary zzm-btn--lg">大 · 44 · 圆角 12</button>
<button type="button" class="zzm-btn zzm-btn--icon" title="图标键">✦</button>
<button type="button" class="zzm-btn zzm-btn--ghost zzm-btn--sm zzm-btn--icon" title="小图标键">
  ‹
</button>
```

## 等待与禁用：两种语义两张脸

`busy` 是「正在做」：出旋转环、一道反光扫过、饱和度略降、不可点，但**保留变体本色**。`disabled` 是「不能做」：灰面灰字。禁用**永远不用 opacity 淡化**——白字 × 半透明 × 灰面会让字直接消失。任何点击后要等的按钮都用 `busy`。

### 样张 · busy 与 disabled

React：

```jsx
import { Btn, btnClass } from './components/v4/Button.jsx'

export function States() {
  return (
    <>
      <Btn busy>Saving…</Btn>
      <Btn busy variant="primary">保存中…</Btn>
      <Btn busy variant="accent">提交中…</Btn>
      <Btn disabled>禁用</Btn>
      <Btn variant="primary" disabled>禁用 · 灰面灰字</Btn>
    </>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<button type="button" disabled="" class="zzm-btn zzm-btn--busy">Saving…</button>
<button type="button" disabled="" class="zzm-btn zzm-btn--primary zzm-btn--busy">保存中…</button>
<button type="button" disabled="" class="zzm-btn zzm-btn--accent zzm-btn--busy">提交中…</button>
<button type="button" disabled="" class="zzm-btn">禁用</button>
<button type="button" disabled="" class="zzm-btn zzm-btn--primary">禁用 · 灰面灰字</button>
```

## 链接长成键

`<a>`、`<label>` 这类非 button 元素用 `btnClass()` 取同一套类名，质感完全一致。

### 样张 · btnClass()

React：

```jsx
import { Btn, btnClass } from './components/v4/Button.jsx'

export function AsLink() {
  return (
    <>
      <a href="#pricing" className={btnClass({ variant: 'primary' })}>查看定价</a>
      <a href="#docs" className={btnClass({ size: 'sm' })}>阅读文档</a>
    </>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<a href="#pricing" class="zzm-btn zzm-btn--primary">查看定价</a>
<a href="#docs" class="zzm-btn zzm-btn--sm">阅读文档</a>
```

## 属性

| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| `variant` | paper · primary · accent · ghost · ondark | paper | 五种键面 |
| `size` | sm · md · lg | md | 高 28 / 36 / 44；lg 圆角 12 |
| `icon` | boolean | false | 正方形图标键，宽＝高 |
| `block` | boolean | false | 撑满容器宽度 |
| `busy` | boolean | false | 等待态：旋转环 + 反光 + 本色 + 自动禁用 |
| `disabled` | boolean | false | 不能做：灰面灰字 |
| `className` | string | '' | 追加类名；style 只放布局，不放背景和阴影 |

## 铁律

- ✓ 要：圆角矩形：`sm`/`md` 圆角 8，`lg` 圆角 12
- ✗ 不要：把文字按钮做成 999 胶囊——胶囊只留给不可点的徽章
- ✓ 要：光影只从 `.zzm-btn` 取，调用方 `style` 只写 margin / flex / width
- ✗ 不要：在 `style` 里散写 `box-shadow`、渐变或 `border-radius` 盖掉质感
- ✓ 要：点击后要等待的操作用 `busy`
- ✗ 不要：用 `opacity` 淡化来表示禁用
- ✓ 要：一屏只有一个墨键或朱键做主操作
- ✗ 不要：朱键当装饰色——它只标「下一步」或危险动作

## 本页用到的 CSS

按样张里出现的类名，从源文件原样裁出（完整版见 zzm.css）：

```css
/* controls.css */

.zzm-v4 .zzm-btn {
  -webkit-appearance: none;
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 36px;
  padding: 0 16px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .2px;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  border-radius: 8px;                                   /* 圆角矩形——按钮禁胶囊 */
  border: 1px solid rgba(0, 0, 0, .14);
  color: #3F3F3B;
  background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .55),                 /* ④ 反光光晕环 */
    0 1px 2px rgba(0, 0, 0, .09),                       /* ③ 近影 */
    0 2px 6px rgba(0, 0, 0, .06),                       /* ③ 远影 */
    inset 0 1px 0 rgba(255, 255, 255, .92),             /* ② 顶部反光 */
    inset 0 -1px 0 rgba(0, 0, 0, .05);                  /* ① 底缘收沉 */
  transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
    border-color .16s ease, filter .16s ease, opacity .16s ease;
}

.zzm-v4 .zzm-btn:hover {
  transform: translateY(-1px);
  border-color: rgba(0, 0, 0, .18);
  box-shadow:
    0 0 0 4px rgba(255, 255, 255, .8),
    0 2px 4px rgba(0, 0, 0, .10),
    0 4px 12px rgba(0, 0, 0, .09),
    inset 0 1px 0 rgba(255, 255, 255, .95),
    inset 0 -1px 0 rgba(0, 0, 0, .05);
}

.zzm-v4 .zzm-btn:active {
  transform: translateY(0);
  background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);   /* ⑤ 凹面翻转 */
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .45),
    0 1px 0 rgba(255, 255, 255, .65),
    inset 0 2px 4px rgba(0, 0, 0, .13),
    inset 0 1px 2px rgba(0, 0, 0, .09);
}

.zzm-v4 .zzm-btn:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px rgba(166, 64, 47, .20),                   /* 键盘焦点＝朱色光晕 */
    0 1px 2px rgba(0, 0, 0, .09),
    0 2px 6px rgba(0, 0, 0, .06),
    inset 0 1px 0 rgba(255, 255, 255, .92);
}

.zzm-v4 .zzm-btn:disabled {
  cursor: not-allowed;
  transform: none;
}

.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
  color: #A2A29C;
  background: #F2F2EF;
  border-color: rgba(0, 0, 0, .08);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}

.zzm-v4 .zzm-btn--ondark:disabled:not(.zzm-btn--busy) {
  color: rgba(255, 255, 255, .45);
  background: rgba(255, 255, 255, .08);
  border-color: rgba(255, 255, 255, .16);
  box-shadow: none;
}

.zzm-v4 .zzm-btn--busy {
  position: relative;
  overflow: hidden;
  pointer-events: none;
  opacity: .92;
  filter: saturate(.72);
  transform: none;
}

.zzm-v4 .zzm-btn--busy::before {
  content: "";
  position: relative;
  z-index: 1;                                            /* 旋转环压在反光层之上 */
  width: 12px;
  height: 12px;
  flex: none;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: .7;
  animation: zzmBtnSpin .7s linear infinite;
}

.zzm-v4 .zzm-btn--busy::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transform: translateX(-180%);
  background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .45) 50%, transparent 60%);
  animation: zzmPillShine 4.4s ease-in-out infinite;
}

.zzm-v4 .zzm-btn--primary {
  color: #FFFFFF;
  border-color: rgba(0, 0, 0, .85);
  background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%);
  box-shadow:
    0 0 0 3px rgba(22, 22, 22, .06),
    0 1px 2px rgba(0, 0, 0, .18),
    0 3px 10px rgba(22, 22, 22, .24),
    inset 0 1px 0 rgba(255, 255, 255, .20),
    inset 0 -1px 0 rgba(0, 0, 0, .38);
}

.zzm-v4 .zzm-btn--primary:hover {
  filter: brightness(1.12);
  border-color: rgba(0, 0, 0, .85);
  box-shadow:
    0 0 0 4px rgba(22, 22, 22, .09),
    0 2px 4px rgba(0, 0, 0, .20),
    0 5px 14px rgba(22, 22, 22, .28),
    inset 0 1px 0 rgba(255, 255, 255, .24),
    inset 0 -1px 0 rgba(0, 0, 0, .38);
}

.zzm-v4 .zzm-btn--primary:active {
  filter: none;
  background: linear-gradient(180deg, #101010 0%, #22221F 100%);
  box-shadow:
    0 0 0 3px rgba(22, 22, 22, .06),
    inset 0 2px 5px rgba(0, 0, 0, .55),
    inset 0 1px 2px rgba(0, 0, 0, .4);
}

.zzm-v4 .zzm-btn--accent {
  color: #FFFFFF;
  border-color: rgba(122, 42, 29, .9);
  background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%);
  box-shadow:
    0 0 0 3px rgba(166, 64, 47, .10),                   /* 朱色反光光晕 */
    0 1px 2px rgba(0, 0, 0, .14),
    0 3px 10px rgba(166, 64, 47, .28),
    inset 0 1px 0 rgba(255, 255, 255, .32),
    inset 0 -1px 0 rgba(0, 0, 0, .20);
}

.zzm-v4 .zzm-btn--accent:hover {
  filter: brightness(1.06);
  border-color: rgba(122, 42, 29, .9);
  box-shadow:
    0 0 0 4px rgba(166, 64, 47, .16),
    0 2px 4px rgba(0, 0, 0, .16),
    0 5px 14px rgba(166, 64, 47, .34),
    inset 0 1px 0 rgba(255, 255, 255, .36),
    inset 0 -1px 0 rgba(0, 0, 0, .20);
}

.zzm-v4 .zzm-btn--accent:active {
  filter: none;
  background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%);
  box-shadow:
    0 0 0 3px rgba(166, 64, 47, .10),
    inset 0 2px 5px rgba(0, 0, 0, .34),
    inset 0 1px 2px rgba(0, 0, 0, .24);
}

.zzm-v4 .zzm-btn--ghost {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
  color: #4A4A46;
}

.zzm-v4 .zzm-btn--ghost:hover {
  transform: translateY(-1px);
  border-color: rgba(0, 0, 0, .14);
  background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .55),
    0 1px 2px rgba(0, 0, 0, .09),
    0 2px 6px rgba(0, 0, 0, .06),
    inset 0 1px 0 rgba(255, 255, 255, .92);
}

.zzm-v4 .zzm-btn--ghost:active {
  transform: translateY(0);
  border-color: rgba(0, 0, 0, .14);
  background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12);
}

.zzm-v4 .zzm-btn--ondark {
  color: #FFFFFF;
  border-color: rgba(255, 255, 255, .28);
  background: linear-gradient(180deg, rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .06) 100%);
  box-shadow:
    0 0 0 3px rgba(255, 255, 255, .05),
    0 2px 8px rgba(0, 0, 0, .4),
    inset 0 1px 0 rgba(255, 255, 255, .28),
    inset 0 -1px 0 rgba(0, 0, 0, .3);
}

.zzm-v4 .zzm-btn--ondark:hover {
  border-color: rgba(255, 255, 255, .4);
  background: linear-gradient(180deg, rgba(255, 255, 255, .22) 0%, rgba(255, 255, 255, .10) 100%);
  box-shadow:
    0 0 0 4px rgba(255, 255, 255, .08),
    0 3px 10px rgba(0, 0, 0, .45),
    inset 0 1px 0 rgba(255, 255, 255, .34),
    inset 0 -1px 0 rgba(0, 0, 0, .3);
}

.zzm-v4 .zzm-btn--ondark:active {
  background: linear-gradient(180deg, rgba(0, 0, 0, .28) 0%, rgba(255, 255, 255, .04) 100%);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .5);
}

.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }

.zzm-v4 .zzm-btn--lg { height: 44px; padding: 0 24px; font-size: 14px; border-radius: 12px; gap: 8px; }

.zzm-v4 .zzm-btn--icon { width: 36px; padding: 0; }

.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }

.zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; }

/* keyframes */
@keyframes zzmBtnSpin { to { transform: rotate(360deg) } }
@keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } }
```

---

上一页：[图标](https://ui.zhaozimin.cn/foundations/icons.md) · 下一页：[视频播放键](https://ui.zhaozimin.cn/components/video-play.md) · 全站目录：https://ui.zhaozimin.cn/llms.txt
