粘性堆叠卡片
滚动时,每张卡片都停在比上一张略低的位置,前面的卡片缩小,整齐地叠成一摞。
在框内滚动 ↓01需求 02草图 03开发 04上线
用 CSS 在 React 中制作滚动时堆叠的卡片。给每张卡片设置 position: sticky 和 top: calc(16px + var(--i) * 14px),让后面的卡片停在略低的位置,像一副牌一样压住前面的卡片。用滚动驱动动画让被覆盖的卡片缩小:animation: shrink linear both; animation-timeline: view(); animation-range: exit-crossing 0% exit-crossing 60%;,keyframe 终点为 transform: scale(0.9) 和 filter: brightness(0.7)。为不支持 animation-timeline 的浏览器保留降级效果(只堆叠不缩小)。卡片是编号步骤“需求”“草图”“开发”“上线”。在 prefers-reduced-motion 下保留粘性堆叠,去掉缩放。
源代码
'use client'
import s from './demo.module.css'
export default function StickyStack({ labels }: { labels: Record<string, string> }) {
const steps = labels.steps.split('|')
return (
<div className={s.frame} tabIndex={0} aria-label={labels.hint}>
<span className={s.hint}>{labels.hint} ↓</span>
{steps.map((step, i) => (
<article key={step} className={s.card} style={{ '--i': i } as React.CSSProperties}>
<span className={s.n}>{String(i + 1).padStart(2, '0')}</span>
<strong className={s.step}>{step}</strong>
</article>
))}
<div className={s.end} />
</div>
)
}
.frame {
position: absolute;
inset: 0;
display: grid;
justify-items: center;
gap: 28px;
padding: 16px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
}
.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }
.hint { font: 500 var(--text-xs) / 1 var(--font-mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--stage-muted); }
.card {
position: sticky;
top: calc(16px + var(--i) * 14px);
display: flex;
align-items: center;
justify-content: space-between;
width: min(240px, 100%);
height: 110px;
padding: 0 20px;
border: 1px solid var(--stage-rule);
border-radius: 16px;
background: var(--stage-surface);
color: var(--stage-ink);
transform-origin: 50% 0;
}
.card:nth-of-type(even) { background: var(--stage-ink); color: var(--stage-bg); }
@supports (animation-timeline: view()) {
.card {
animation: shrink linear both;
animation-timeline: view();
animation-range: exit-crossing 0% exit-crossing 60%;
}
}
@keyframes shrink { to { transform: scale(0.9); filter: brightness(0.7); } }
.n { font: 500 var(--text-sm) / 1 var(--font-mono); color: var(--stage-accent); }
.step { font: 600 var(--text-xl) / 1 var(--font-display); }
.end { height: 120px; }
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}