enter/exit x ±24 · height 0→auto · 250ms
List AnimatePresence
リストへの追加・削除・シャッフルを AnimatePresence と layout で表現する。要素の出入りと並び替えが spring でつながる。
- Inbox
- Followups
- Archive
/**
* List AnimatePresence — リスト要素の出入り choreography(add / remove / shuffle)。
* 移植元: motion-playground app/_sections/ListAnimatePresence.tsx(dark 変換 M-B-2)
*/
import { AnimatePresence, motion } from 'motion/react';
import { useState } from 'react';
import { easings } from '../../../lib/motion-tokens/playground';
type Item = { id: number; label: string };
const seed: Item[] = [
{ id: 1, label: 'Inbox' },
{ id: 2, label: 'Followups' },
{ id: 3, label: 'Archive' },
];
const candidates = [
'Drafts',
'Sent',
'Snoozed',
'Spam',
'Trash',
'Important',
'All mail',
];
const BTN_PRIMARY =
'rounded-md bg-[var(--text-primary)] px-3 py-1.5 text-sm font-medium text-[var(--text-inverted)] transition-colors hover:bg-[var(--text-secondary)]';
const BTN_GHOST =
'rounded-md border border-[var(--border-subtle)] bg-[var(--bg-elevated)] px-3 py-1.5 text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--border-subtle)]';
export default function ListAnimatePresence() {
const [items, setItems] = useState<Item[]>(seed);
const [nextId, setNextId] = useState(seed.length + 1);
function addItem() {
const label = candidates[(nextId - 1) % candidates.length];
setItems((prev) => [...prev, { id: nextId, label }]);
setNextId((n) => n + 1);
}
function removeItem(id: number) {
setItems((prev) => prev.filter((it) => it.id !== id));
}
function shuffle() {
setItems((prev) => [...prev].sort(() => Math.random() - 0.5));
}
return (
<div data-lab-island className="w-full p-4 lg:p-6">
<div className="mb-4 flex flex-wrap gap-2">
<button type="button" onClick={addItem} className={BTN_PRIMARY}>
+ アイテムを追加
</button>
<button type="button" onClick={shuffle} className={BTN_GHOST}>
⇅ シャッフル
</button>
<button
type="button"
onClick={() => setItems(seed)}
className={BTN_GHOST}
>
リセット
</button>
</div>
<div className="rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-3">
<motion.ul layout className="space-y-2">
<AnimatePresence initial={false}>
{items.map((it) => (
<motion.li
key={it.id}
layout
initial={{ opacity: 0, x: -24, height: 0 }}
animate={{ opacity: 1, x: 0, height: 'auto' }}
exit={{ opacity: 0, x: 24, height: 0 }}
transition={{
duration: 0.25,
ease: easings.md3Emphasized,
}}
className="flex items-center justify-between rounded-md bg-[var(--bg-elevated)] px-3 py-2"
>
<span className="text-sm text-[var(--text-primary)]">
{it.label}
</span>
<button
type="button"
onClick={() => removeItem(it.id)}
className="rounded p-1 text-xs text-[var(--text-muted)] hover:bg-[var(--border-subtle)] hover:text-[var(--text-secondary)]"
aria-label={`${it.label} を削除`}
>
✕
</button>
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</div>
</div>
);
} Implement an animated list with motion/react:
- wrap the list in AnimatePresence with initial={false}
- each item: initial { opacity: 0, x: -24, height: 0 } -> animate { opacity: 1, x: 0, height: 'auto' } -> exit { opacity: 0, x: 24, height: 0 }
- transition 250ms, ease cubic-bezier(0.05, 0.7, 0.1, 1)
- add `layout` to every item so siblings slide smoothly on add / remove / reorder
- shuffle animates positions via the layout spring
- drive add / remove / shuffle / reset from React state