whileInView · amount 0.4 · 500ms
Scroll Reveal
スクロールでビューポートに入るたびカードが fade + slide インする。once: false なので何度でも再発火する。
Insight
scroll で発火する viewport reveal
Pattern
whileInView で1回 or 何度でも
Stagger
viewport amount を調整
Performance
GPU layer に乗せる
A11y
prefers-reduced-motion 尊重
Best
30% 表示で発火が標準
/**
* Scroll Reveal — whileInView で viewport 進入/退出のたびに再発火するカード。
* 移植元: motion-playground app/_sections/ScrollReveal.tsx(dark 変換 M-B-2)
* stage 内スクロールで表示されるたび発火するため useLabReplay は不要。
*/
import { motion } from 'motion/react';
import { easings } from '../../../lib/motion-tokens/playground';
const cards = [
{ title: 'Insight', body: 'scroll で発火する viewport reveal' },
{ title: 'Pattern', body: 'whileInView で1回 or 何度でも' },
{ title: 'Stagger', body: 'viewport amount を調整' },
{ title: 'Performance', body: 'GPU layer に乗せる' },
{ title: 'A11y', body: 'prefers-reduced-motion 尊重' },
{ title: 'Best', body: '30% 表示で発火が標準' },
];
export default function ScrollReveal() {
return (
<div
data-lab-island
className="grid w-full gap-4 p-4 sm:grid-cols-2 lg:p-6"
>
{cards.map((card, i) => (
<motion.div
key={card.title}
initial={{ opacity: 0, y: 32 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.4 }}
transition={{
duration: 0.5,
ease: easings.md3Emphasized,
delay: (i % 2) * 0.08,
}}
className="rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-5"
>
<p className="text-sm font-medium text-[var(--text-primary)]">{card.title}</p>
<p className="mt-1 text-xs text-[var(--text-muted)]">{card.body}</p>
</motion.div>
))}
</div>
);
} Implement scroll reveal with motion/react:
- use whileInView with viewport { amount: 0.4, once: false }
- initial { opacity: 0, y: 32 } -> whileInView { opacity: 1, y: 0 }
- 500ms, ease cubic-bezier(0.05, 0.7, 0.1, 1)
- in a 2-column grid, offset even/odd items by 80ms delay
- re-fires every time the element re-enters the viewport