组件 · COMPONENT
按钮 Btn
光从正上方来——每个按钮都是一枚真实的键:凸面渐变、顶部反光、贴地投影、反光光晕、按压内陷。文字按钮一律圆角矩形,禁胶囊。
试验台
三条轴自由组合:五种键面 × 三档高度 × 三种状态。下方同时给出对应的 JSX 和类名。
<Btn variant="primary">墨键</Btn><button class="zzm-btn zzm-btn--primary">
五种键面
纸键是默认;墨键给一屏里最主要的操作;朱键只给品牌强调、播放或破坏性确认;幽灵键静默时是文字,悬停才「长成键」,用于工具条;暗面键只在深色面上用。
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>
</>
)
}<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>/* 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--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);
}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>
</>
)
}<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>/* 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--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--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);
}三档高度
高度落在 4pt 网格上:sm 28(工具条)、md 36(表单与一般操作)、lg 44(主操作,圆角升到 12)。icon 把键变成正方形。
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>
</>
)
}<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>/* 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--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--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--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; }等待与禁用:两种语义两张脸
busy 是「正在做」:出旋转环、一道反光扫过、饱和度略降、不可点,但保留变体本色。disabled 是「不能做」:灰面灰字。禁用永远不用 opacity 淡化——白字 × 半透明 × 灰面会让字直接消失。任何点击后要等的按钮都用 busy。
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>
</>
)
}<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>/* 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);
}
/* keyframes */
@keyframes zzmBtnSpin { to { transform: rotate(360deg) } }
@keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } }链接长成键
<a>、<label> 这类非 button 元素用 btnClass() 取同一套类名,质感完全一致。
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>
</>
)
}<a href="#pricing" class="zzm-btn zzm-btn--primary">查看定价</a>
<a href="#docs" class="zzm-btn zzm-btn--sm">阅读文档</a>/* 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--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--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }
.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }属性
| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| 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 淡化来表示禁用✓ 要一屏只有一个墨键或朱键做主操作
✗ 不要朱键当装饰色——它只标「下一步」或危险动作