弹性开关
开关圆钮在移动时会拉长成胶囊形,到另一侧后恢复圆形,轨道同时填满颜色。
用 CSS 为 React 制作一个符合无障碍规范的开关。使用真正的 <button role="switch" aria-checked>,旁边显示可见文字“邮件通知”。轨道尺寸 48×28px;22px 的圆钮关闭时位于 translateX(3px),开启时位于 translateX(23px),移动时长 260ms,缓动为 cubic-bezier(0.16, 1, 0.3, 1)。按下时(:active),圆钮从即将离开的一侧做 scaleX(1.25) 拉伸,看起来像被拉长。轨道在 200ms 内从中性底色渐变为强调色。:focus-visible 时显示清晰的焦点环。点击文字也能切换。在 prefers-reduced-motion 下圆钮直接跳到位,不做拉伸。
源代码
'use client'
import { useId, useState } from 'react'
import s from './demo.module.css'
export default function ToggleSwitch({ labels }: { labels: Record<string, string> }) {
const [on, setOn] = useState(true)
const id = useId()
return (
<div className={s.row}>
<label htmlFor={id} className={s.label}>
{labels.label}
</label>
<button id={id} type="button" role="switch" aria-checked={on} className={s.track} onClick={() => setOn(!on)}>
<span className={s.knob} />
</button>
</div>
)
}
.row {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 18px;
border: 1px solid var(--stage-rule);
border-radius: 14px;
background: var(--stage-surface);
}
.label { color: var(--stage-ink); font: 500 var(--text-base) / 1.2 var(--font-body); white-space: nowrap; cursor: pointer; }
.track {
position: relative;
flex: none;
width: 48px;
height: 28px;
padding: 0;
border: 0;
border-radius: 999px;
background: var(--stage-rule);
cursor: pointer;
transition: background-color 200ms var(--ease-out);
}
.track[aria-checked='true'] { background: var(--stage-accent); }
.track:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }
.knob {
position: absolute;
top: 3px;
left: 0;
width: 22px;
height: 22px;
border-radius: 999px;
background: var(--stage-ink);
transform: translateX(3px);
transform-origin: left;
transition: transform 260ms var(--ease-out);
}
.track[aria-checked='true'] .knob { transform: translateX(23px); transform-origin: right; }
.track:active .knob { transform: translateX(3px) scaleX(1.25); }
.track[aria-checked='true']:active .knob { transform: translateX(23px) scaleX(1.25); }
@media (prefers-reduced-motion: reduce) {
.knob, .track { transition: none; }
.track:active .knob { transform: translateX(3px); }
.track[aria-checked='true']:active .knob { transform: translateX(23px); }
}