手风琴与折叠设计
单开 vs 多开手风琴、高度动画、可折叠区段的 ARIA 接线。
组件模式入门
为什么
手风琴让用户扫区段标题并只深入所需内容,减少长页压倒。对非同级内容比标签页便宜,对可选细节比模态便宜。
复合组件模式被 Radix UI、Headless UI 和 shadcn/ui 采用 — 这是 2024 年采用最多的三个无头库。相比扁平组件 API,它们减少 70% 的属性逐层传递。
何时使用
用于 3-10 区段且用户一次需一两个(FAQ、高级设置)。用户须从头读到尾的内容避免 — 全展开更好。
如何做
1. ARIA — button aria-expanded={open}、region aria-labelledby 指向 button id
2. 开放模式 — 单开(开一个关其他)或多开(独立);FAQ 默认单开
3. 触发器 — 真正 <button> 支持键盘+屏幕阅读器,绝不用 div
4. 动画 — 用 grid-template-rows 0fr→1fr 动画高度(无需测量),或 opacity+translate
5. 默认展开 — 首区段或全无;绝不全开(违背目的)
6. 图标 — 折叠时箭头旋转 90°,aria-hidden,仅装饰
7. 减弱动效 — prefers-reduced-motion 下动画即时
示例
function Accordion({ items, mode = "single" }: { items: Item[]; mode?: "single" | "multi" }) {
const [open, setOpen] = useState<Set<string>>(new Set());
const toggle = (id: string) => {
setOpen((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else {
if (mode === "single") next.clear();
next.add(id);
}
return next;
});
};
return items.map((it) => (
<div key={it.id}>
<button aria-expanded={open.has(it.id)} aria-controls={`p-${it.id}`} onClick={() => toggle(it.id)}>
{it.title} <span aria-hidden className={open.has(it.id) ? "rotate-90" : ""}>▸</span>
</button>
<div id={`p-${it.id}`} role="region" aria-labelledby={`b-${it.id}`}
className="grid transition-all" style={{ gridTemplateRows: open.has(it.id) ? "1fr" : "0fr" }}>
<div className="overflow-hidden">{it.content}</div>
</div>
</div>
));
}
常见问题
- ⚠用猜值动画 max-height — 超过猜值的内容被裁
- ⚠div 触发器加 onClick — 不可键盘操作,屏幕阅读器说 '可点击' 而非 '按钮'
- ⚠默认全开 — 页面巨大,违背扫标题收益
最佳实践
- ✓用 grid-template-rows 0fr→1fr 动画 — 无高度测量、无裁切、GPU 便宜
- ✓用真正 <button> 触发器 — 免费获键盘+屏幕阅读器支持
- ✓FAQ 默认单开 — 多开用于用户比较区段的设置