๐ฆ View Transitions API
Morph Gallery
Baseline across all major browsers. Shared-element morphing โ the signature move of native apps โ now belongs to the web, without an animation library.
Use it for: Galleries ยท e-commerce product grids ยท app-like dashboards
The complete source
This is the exact file rendering the demo above โ a single self-contained React client component with its styles inline. Copy or download it, drop it into any React / Next.js project, and it runs. No extra dependencies.
transitions.jsx
1"use client";2import { useState } from "react";34/* ============================================================5MORPH GALLERY โ the View Transitions API (same-document).6Baseline across Chrome, Edge, Safari 18+ and Firefox 144+.7document.startViewTransition() snapshots the old DOM, we swap8state, and the browser MORPHS elements that share a9view-transition-name. Zero animation libraries.10============================================================ */1112const CARDS = [13{ id: "violet", name: "The Seer", hue: "#7c3aed", desc: "Sees ten moves ahead โ strategy as second sight." },14{ id: "cyan", name: "The Current", hue: "#06b6d4", desc: "Flow state embodied. Moves around obstacles like water." },15{ id: "gold", name: "The Spark", hue: "#ffa500", desc: "One idea, properly lit, can illuminate an era." },16{ id: "rose", name: "The Pulse", hue: "#f43f5e", desc: "Feels the room's rhythm and conducts it like a score." },17];1819export default function Transitions() {20const [open, setOpen] = useState(null);2122const go = (next) => {23// Graceful: browsers without the API just switch state instantly.24if (document.startViewTransition) {25document.startViewTransition(() => setOpen(next));26} else {27setOpen(next);28}29};3031const card = CARDS.find((c) => c.id === open);3233return (34<div className="vt-wrap">35<style>{`36.vt-wrap { min-height: 60svh; }37.vt-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.6rem; }38.vt-card {39border-radius: 20px; padding: 2.4rem 1.6rem; cursor: pointer;40border: 1px solid rgba(255,255,255,.15); text-align: center;41transition: border-color .3s;42background: rgba(255,255,255,.04);43}44.vt-card:hover { border-color: rgba(255,255,255,.45); }45.vt-orb {46width: 84px; height: 84px; border-radius: 50%; margin: 0 auto 1.4rem;47box-shadow: 0 10px 40px -8px currentColor;48}49.vt-name { font-size: 1.8rem; font-weight: 800; }50.vt-hint { font-size: 1.2rem; color: rgba(255,255,255,.5); margin-top: .6rem; }5152.vt-detail {53border-radius: 28px; padding: 4rem 2.4rem; text-align: center;54border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.05);55}56.vt-detail .vt-orb { width: 180px; height: 180px; margin-bottom: 2.4rem; }57.vt-detail h3 { font-size: 3.6rem; font-weight: 900; }58.vt-detail p { font-size: 1.7rem; color: rgba(255,255,255,.8); max-width: 48ch; margin: 1.6rem auto 2.4rem; line-height: 1.7; }59.vt-back {60font-size: 1.4rem; font-weight: 700; padding: 1rem 2.4rem; border-radius: 999px;61border: 1px solid rgba(255,255,255,.3); color: #fff; background: transparent; cursor: pointer;62}63.vt-back:hover { background: rgba(255,255,255,.1); }6465/* The magic: shared element names = browser-computed morphs. */66::view-transition-group(*) { animation-duration: .45s; }67`}</style>6869{!card ? (70<div className="vt-grid">71{CARDS.map((c) => (72<button73key={c.id}74className="vt-card"75onClick={() => go(c.id)}76style={{ color: c.hue }}77>78<div79className="vt-orb"80style={{81background: `radial-gradient(circle at 30% 30%, #fff3, transparent 40%), ${c.hue}`,82viewTransitionName: `orb-${c.id}`,83}}84/>85<div86className="vt-name"87style={{ viewTransitionName: `name-${c.id}`, color: "#fff" }}88>89{c.name}90</div>91<div className="vt-hint">tap to morph โ</div>92</button>93))}94</div>95) : (96<div className="vt-detail" style={{ color: card.hue }}>97<div98className="vt-orb"99style={{100background: `radial-gradient(circle at 30% 30%, #fff3, transparent 40%), ${card.hue}`,101viewTransitionName: `orb-${card.id}`,102margin: "0 auto",103}}104/>105<h3 style={{ viewTransitionName: `name-${card.id}`, color: "#fff" }}>106{card.name}107</h3>108<p>{card.desc}</p>109<button className="vt-back" onClick={() => go(null)}>110โ back to the gallery111</button>112</div>113)}114</div>115);116}117