๐Ÿฆ‹ 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";
3
4/* ============================================================
5 MORPH GALLERY โ€” the View Transitions API (same-document).
6 Baseline across Chrome, Edge, Safari 18+ and Firefox 144+.
7 document.startViewTransition() snapshots the old DOM, we swap
8 state, and the browser MORPHS elements that share a
9 view-transition-name. Zero animation libraries.
10 ============================================================ */
11
12const 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];
18
19export default function Transitions() {
20 const [open, setOpen] = useState(null);
21
22 const go = (next) => {
23 // Graceful: browsers without the API just switch state instantly.
24 if (document.startViewTransition) {
25 document.startViewTransition(() => setOpen(next));
26 } else {
27 setOpen(next);
28 }
29 };
30
31 const card = CARDS.find((c) => c.id === open);
32
33 return (
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 {
39 border-radius: 20px; padding: 2.4rem 1.6rem; cursor: pointer;
40 border: 1px solid rgba(255,255,255,.15); text-align: center;
41 transition: border-color .3s;
42 background: rgba(255,255,255,.04);
43 }
44 .vt-card:hover { border-color: rgba(255,255,255,.45); }
45 .vt-orb {
46 width: 84px; height: 84px; border-radius: 50%; margin: 0 auto 1.4rem;
47 box-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; }
51
52 .vt-detail {
53 border-radius: 28px; padding: 4rem 2.4rem; text-align: center;
54 border: 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 {
60 font-size: 1.4rem; font-weight: 700; padding: 1rem 2.4rem; border-radius: 999px;
61 border: 1px solid rgba(255,255,255,.3); color: #fff; background: transparent; cursor: pointer;
62 }
63 .vt-back:hover { background: rgba(255,255,255,.1); }
64
65 /* The magic: shared element names = browser-computed morphs. */
66 ::view-transition-group(*) { animation-duration: .45s; }
67 `}</style>
68
69 {!card ? (
70 <div className="vt-grid">
71 {CARDS.map((c) => (
72 <button
73 key={c.id}
74 className="vt-card"
75 onClick={() => go(c.id)}
76 style={{ color: c.hue }}
77 >
78 <div
79 className="vt-orb"
80 style={{
81 background: `radial-gradient(circle at 30% 30%, #fff3, transparent 40%), ${c.hue}`,
82 viewTransitionName: `orb-${c.id}`,
83 }}
84 />
85 <div
86 className="vt-name"
87 style={{ 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 <div
98 className="vt-orb"
99 style={{
100 background: `radial-gradient(circle at 30% 30%, #fff3, transparent 40%), ${card.hue}`,
101 viewTransitionName: `orb-${card.id}`,
102 margin: "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 gallery
111 </button>
112 </div>
113 )}
114 </div>
115 );
116}
117