Review

Shared-layout expand for plant specimen cards

Botanic shop catalog feels abrupt when a specimen opens into the detail sheet. Add framer-motion shared layout morph from grid card → expanded panel so the photo and title glide in place. Each specimen gets its own layoutId pair so only the opened card animates.

ReactTier 1framer-motionlayoutfrontend

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

components/catalog/SpecimenGrid.tsx+19-10
2222export function SpecimenGrid({ specimens }: { specimens: Specimen[] }) {
2323 const [openId, setOpenId] = useState<string | null>(null);
2424 const open = specimens.find((s) => s.id === openId) ?? null;
2525
2626 return (
2727 <LayoutGroup>
2828 <ul className="specimen-grid">
2929 {specimens.map((s) => (
30- <li key={s.id} onClick={() => setOpenId(s.id)}>
31- <img src={s.thumbUrl} alt={s.latinName} />
32- <h3>{s.commonName}</h3>
33- </li>
30+ <li key={s.id}>
31+ <button type="button" onClick={() => setOpenId(s.id)} className="specimen-card">
32+ <motion.img layoutId={`specimen-photo-${s.id}`} src={s.thumbUrl} alt={s.latinName} />
33+ <motion.h3 layoutId={`specimen-title-${s.id}`}>{s.commonName}</motion.h3>
34+ </button>
35+ </li>
3436 ))}
3537 </ul>
3638 <AnimatePresence>
3739 {open && (
38- <div className="specimen-sheet" role="dialog" aria-modal="true">
39- <img src={open.heroUrl} alt={open.latinName} />
40- <h2>{open.commonName}</h2>
41- <p>{open.careNotes}</p>
42- <button type="button" onClick={() => setOpenId(null)}>Close</button>
43- </div>
40+ <motion.div
41+ className="specimen-sheet"
42+ role="dialog"
43+ aria-modal="true"
44+ initial={{ opacity: 0 }}
45+ animate={{ opacity: 1 }}
46+ exit={{ opacity: 0 }}
47+ >
48+ <motion.img layoutId={`specimen-photo-${open.id}`} src={open.heroUrl} alt={open.latinName} />
49+ <motion.h2 layoutId={`specimen-title-${open.id}`}>{open.commonName}</motion.h2>
50+ <p>{open.careNotes}</p>
51+ <button type="button" onClick={() => setOpenId(null)}>Close</button>
52+ </motion.div>
4453 )}
4554 </AnimatePresence>
4655 </LayoutGroup>
4756 );
4857}