*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #070a12;
--panel: #121a2b;
--border: #263249;
--text: #f0f4fb;
--muted: #8a95a8;
--accent: #86e8ff;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* ── Views ── */
.view {
display: none;
min-height: 100vh;
}
.view.active {
display: block;
}
/* ── Page header ── */
.page-header {
text-align: center;
padding: 5rem 2rem 2rem;
}
.eyebrow {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--accent);
margin-bottom: 1rem;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
letter-spacing: -0.02em;
}
.subtitle {
font-size: clamp(0.9rem, 2vw, 1.05rem);
color: var(--muted);
max-width: 480px;
margin: 0.75rem auto 0;
line-height: 1.6;
}
/* ── Card grid ── */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
max-width: 960px;
margin: 2rem auto;
padding: 0 2rem 4rem;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(134, 232, 255, 0.3);
box-shadow: 0 8px 32px rgba(134, 232, 255, 0.06);
}
.card-visual {
position: relative;
height: 180px;
overflow: hidden;
}
.card-image {
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
hsl(var(--hue, 200) 60% 15%),
hsl(var(--hue, 200) 80% 25%),
hsl(calc(var(--hue, 200) + 40) 70% 20%)
);
display: flex;
align-items: center;
justify-content: center;
}
.card-number {
font-size: 3rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.15);
letter-spacing: -0.03em;
}
.card-body {
padding: 1.25rem;
}
.card-body h3 {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.card-body p {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
/* ── Detail view ── */
.detail-layout {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 100vh;
}
.detail-image-area {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
}
.detail-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, hsl(200 60% 15%), hsl(200 80% 25%), hsl(240 70% 20%));
}
.detail-number {
font-size: 8rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.1);
letter-spacing: -0.03em;
}
.detail-content {
padding: 4rem 3rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.back-btn {
align-self: flex-start;
background: none;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--accent);
padding: 0.5rem 1rem;
font: 600 0.8rem/1 'Inter', system-ui, sans-serif;
cursor: pointer;
transition: border-color 0.25s, background 0.25s;
}
.back-btn:hover {
border-color: var(--accent);
background: rgba(134, 232, 255, 0.06);
}
.detail-title {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.1;
}
.detail-desc {
font-size: 1.1rem;
color: var(--muted);
line-height: 1.6;
}
.detail-meta {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.meta-tag {
padding: 0.25rem 0.7rem;
background: rgba(134, 232, 255, 0.1);
border: 1px solid rgba(134, 232, 255, 0.2);
border-radius: 6px;
font-size: 0.72rem;
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.detail-body {
font-size: 0.95rem;
color: var(--muted);
line-height: 1.7;
}
.detail-body code {
background: rgba(134, 232, 255, 0.1);
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.85rem;
color: var(--accent);
}
/* ── View Transition styles ── */
::view-transition-old(root) {
animation: fade-out 0.25s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.25s ease-in;
}
::view-transition-group(card-image-1),
::view-transition-group(card-image-2),
::view-transition-group(card-image-3),
::view-transition-group(card-image-4),
::view-transition-group(card-image-5),
::view-transition-group(card-image-6) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-group(card-title-1),
::view-transition-group(card-title-2),
::view-transition-group(card-title-3),
::view-transition-group(card-title-4),
::view-transition-group(card-title-5),
::view-transition-group(card-title-6) {
animation-duration: 0.35s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* ── Fallback for no View Transitions ── */
.no-vt .view {
transition: opacity 0.3s ease;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.detail-layout {
grid-template-columns: 1fr;
}
.detail-image-area {
position: relative;
height: 50vh;
}
.detail-content {
padding: 2rem 1.5rem;
}
.card-grid {
grid-template-columns: 1fr;
padding: 0 1rem 3rem;
}
}
if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
// ── Demo shell ──
initDemoShell({
title: 'Card Grid Transition',
category: 'transitions',
tech: ['view-transitions-api', 'css'],
});
// ── Check support ──
const supportsVT = typeof document.startViewTransition === 'function';
if (!supportsVT) {
document.body.classList.add('no-vt');
}
// ── Card data ──
const cardData = {
1: { title: 'Kinetic Typography', desc: 'Motion-driven text that responds to scroll, time, and user interaction.', hue: 200 },
2: { title: 'Particle Systems', desc: 'Thousands of elements choreographed into flowing, organic formations.', hue: 270 },
3: { title: 'Scroll Choreography', desc: 'Precisely timed sequences that unfold as the user scrolls through content.', hue: 330 },
4: { title: 'Shader Art', desc: 'GPU-powered visuals that create mesmerizing patterns in real-time.', hue: 45 },
5: { title: '3D Environments', desc: 'Immersive three-dimensional spaces built with WebGL and Three.js.', hue: 160 },
6: { title: 'Magnetic Interactions', desc: 'Elements that attract, repel, and respond to cursor proximity with spring physics.', hue: 15 },
};
// ── DOM refs ──
const gridView = document.getElementById('grid-view');
const detailView = document.getElementById('detail-view');
const detailImage = document.getElementById('detail-image');
const detailNumber = document.getElementById('detail-number');
const detailTitle = document.getElementById('detail-title');
const detailDesc = document.getElementById('detail-desc');
const backBtn = document.getElementById('back-btn');
let currentId = null;
// ── Navigate to detail ──
function showDetail(id) {
const data = cardData[id];
if (!data) return;
currentId = id;
const updateDOM = () => {
// Set view-transition-name on detail elements to match the card
detailImage.style.viewTransitionName = `card-image-${id}`;
detailTitle.style.viewTransitionName = `card-title-${id}`;
// Update detail content
detailImage.style.background = `linear-gradient(135deg, hsl(${data.hue} 60% 15%), hsl(${data.hue} 80% 25%), hsl(${data.hue + 40} 70% 20%))`;
detailNumber.textContent = String(id).padStart(2, '0');
detailTitle.textContent = data.title;
detailDesc.textContent = data.desc;
// Clear view-transition-name on the card's elements so they don't conflict
const cardImage = document.querySelector(`[data-id="${id}"] .card-image`);
const cardTitle = document.querySelector(`[data-id="${id}"] h3`);
if (cardImage) cardImage.style.viewTransitionName = 'none';
if (cardTitle) cardTitle.style.viewTransitionName = 'none';
// Swap views
gridView.classList.remove('active');
detailView.classList.add('active');
};
if (supportsVT && !prefersReducedMotion()) {
document.startViewTransition(updateDOM);
} else {
updateDOM();
}
}
// ── Navigate back to grid ──
function showGrid() {
const id = currentId;
const updateDOM = () => {
// Restore view-transition-name on original card elements
if (id) {
const cardImage = document.querySelector(`[data-id="${id}"] .card-image`);
const cardTitle = document.querySelector(`[data-id="${id}"] h3`);
if (cardImage) cardImage.style.viewTransitionName = `card-image-${id}`;
if (cardTitle) cardTitle.style.viewTransitionName = `card-title-${id}`;
}
// Clear detail transition names
detailImage.style.viewTransitionName = 'none';
detailTitle.style.viewTransitionName = 'none';
// Swap views
detailView.classList.remove('active');
gridView.classList.add('active');
currentId = null;
};
if (supportsVT && !prefersReducedMotion()) {
document.startViewTransition(updateDOM);
} else {
updateDOM();
}
}
// ── Event listeners ──
document.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => {
const id = card.dataset.id;
showDetail(id);
});
});
backBtn.addEventListener('click', showGrid);
// ── Keyboard navigation ──
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && detailView.classList.contains('active')) {
showGrid();
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Grid Transition — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Grid View -->
<div id="grid-view" class="view active">
<header class="page-header">
<span class="eyebrow">Demo 11</span>
<h1>Card Grid Transition</h1>
<p class="subtitle">Click any card to see a shared-element View Transition to the detail page.</p>
</header>
<div class="card-grid">
<article class="card" data-id="1">
<div class="card-visual" style="--hue: 200;">
<div class="card-image" style="view-transition-name: card-image-1;">
<span class="card-number">01</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-1;">Kinetic Typography</h3>
<p>Motion-driven text that responds to scroll, time, and user interaction.</p>
</div>
</article>
<article class="card" data-id="2">
<div class="card-visual" style="--hue: 270;">
<div class="card-image" style="view-transition-name: card-image-2;">
<span class="card-number">02</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-2;">Particle Systems</h3>
<p>Thousands of elements choreographed into flowing, organic formations.</p>
</div>
</article>
<article class="card" data-id="3">
<div class="card-visual" style="--hue: 330;">
<div class="card-image" style="view-transition-name: card-image-3;">
<span class="card-number">03</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-3;">Scroll Choreography</h3>
<p>Precisely timed sequences that unfold as the user scrolls through content.</p>
</div>
</article>
<article class="card" data-id="4">
<div class="card-visual" style="--hue: 45;">
<div class="card-image" style="view-transition-name: card-image-4;">
<span class="card-number">04</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-4;">Shader Art</h3>
<p>GPU-powered visuals that create mesmerizing patterns in real-time.</p>
</div>
</article>
<article class="card" data-id="5">
<div class="card-visual" style="--hue: 160;">
<div class="card-image" style="view-transition-name: card-image-5;">
<span class="card-number">05</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-5;">3D Environments</h3>
<p>Immersive three-dimensional spaces built with WebGL and Three.js.</p>
</div>
</article>
<article class="card" data-id="6">
<div class="card-visual" style="--hue: 15;">
<div class="card-image" style="view-transition-name: card-image-6;">
<span class="card-number">06</span>
</div>
</div>
<div class="card-body">
<h3 style="view-transition-name: card-title-6;">Magnetic Interactions</h3>
<p>Elements that attract, repel, and respond to cursor proximity with spring physics.</p>
</div>
</article>
</div>
</div>
<!-- Detail View (hidden, populated by JS) -->
<div id="detail-view" class="view">
<div class="detail-layout">
<div class="detail-image-area">
<div id="detail-image" class="detail-image">
<span id="detail-number" class="detail-number"></span>
</div>
</div>
<div class="detail-content">
<button id="back-btn" class="back-btn">← Back to grid</button>
<h2 id="detail-title" class="detail-title"></h2>
<p id="detail-desc" class="detail-desc"></p>
<div class="detail-meta">
<span class="meta-tag">View Transitions API</span>
<span class="meta-tag">CSS</span>
<span class="meta-tag">Shared Elements</span>
</div>
<p class="detail-body">This demo uses the View Transitions API to animate between a card grid and a detail page. The card image and title have matching <code>view-transition-name</code> properties, allowing the browser to smoothly interpolate their position and size during the transition.</p>
<p class="detail-body">In browsers that don't support View Transitions, the content swaps instantly with a simple CSS crossfade fallback.</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>