平滑手风琴
面板展开到内容的自然高度后又能收回,无需固定 max-height,箭头也随之转动。
不用。大多数动效都是纯 CSS,GSAP 只在编排时间线时有帮助。
会。每个演示在该设置下都会关闭或减弱动作。
可以。粘贴到任意 AI 编程工具里即可重建这个效果。
用 React 和 CSS 制作一个能动画到内容自然高度的 FAQ 手风琴。每一项有一个带 aria-expanded 和 aria-controls 的标题按钮,以及一个面板区域。面板设为 grid,grid-template-rows: 0fr,内部 div 设置 overflow: hidden 和 min-height: 0;展开时改为 grid-template-rows: 1fr。grid-template-rows 过渡 320ms,缓动 cubic-bezier(0.16, 1, 0.3, 1),内部内容随之淡入。展开时箭头以同样的节奏旋转 180deg。同一时间只允许展开一项。在 prefers-reduced-motion 下直接展开和收起。问题为“一定要用 GSAP 吗?”“会照顾减少动态效果的设置吗?”“可以复制提示词吗?”。
源代码
'use client'
import { useId, useState } from 'react'
import s from './demo.module.css'
export default function Accordion({ labels }: { labels: Record<string, string> }) {
const [open, setOpen] = useState(0)
const id = useId()
const items = [1, 2, 3].map((n) => ({ q: labels[`q${n}`], a: labels[`a${n}`] }))
return (
<div className={s.list}>
{items.map(({ q, a }, i) => (
<div key={q} className={s.item} data-open={open === i || undefined}>
<h3 className={s.heading}>
<button
type="button"
className={s.trigger}
aria-expanded={open === i}
aria-controls={`${id}-${i}`}
onClick={() => setOpen(open === i ? -1 : i)}
>
{q}
<svg className={s.chevron} viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9l6 6 6-6" />
</svg>
</button>
</h3>
<div id={`${id}-${i}`} role="region" className={s.panel}>
<div className={s.inner}>
<p className={s.answer}>{a}</p>
</div>
</div>
</div>
))}
</div>
)
}
.list {
width: min(320px, 100%);
border-top: 1px solid var(--stage-rule);
}
.item { border-bottom: 1px solid var(--stage-rule); }
.heading { margin: 0; }
.trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 11px 2px;
border: 0;
background: transparent;
color: var(--stage-ink);
font: 500 var(--text-sm) / 1.3 var(--font-body);
text-align: left;
cursor: pointer;
}
.trigger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: 4px; }
.chevron {
flex: none;
width: 16px;
height: 16px;
fill: none;
stroke: var(--stage-muted);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 320ms var(--ease-out);
}
.item[data-open] .chevron { transform: rotate(180deg); stroke: var(--stage-accent); }
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 320ms var(--ease-out);
}
.item[data-open] .panel { grid-template-rows: 1fr; }
.inner {
min-height: 0;
overflow: hidden;
opacity: 0;
transition: opacity 240ms var(--ease-out);
}
.item[data-open] .inner { opacity: 1; transition-delay: 80ms; }
.answer {
margin: 0;
padding: 0 2px 12px;
font: 400 var(--text-sm) / 1.5 var(--font-body);
color: var(--stage-muted);
}
@media (prefers-reduced-motion: reduce) {
.panel, .inner, .chevron { transition: none; }
}