QR Assemble
import { useMemo, useState } from 'react';
const css = `
.card-qr-assemble {
display: grid;
justify-items: center;
gap: 10px;
width: 176px;
padding: 14px 14px 12px;
border-radius: 16px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
cursor: pointer;
}
.card-qr-assemble:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-qr-assemble__code {
position: relative;
display: grid;
grid-template-columns: repeat(21, 5px);
grid-auto-rows: 5px;
padding: 6px;
border-radius: 8px;
background: light-dark(#fbfcfd, #121417);
}
.card-qr-assemble__m {
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
transform: scale(0.4);
transition: opacity 0.3s ease var(--d), transform 0.3s cubic-bezier(0.34, 1.5, 0.64, 1) var(--d);
}
.card-qr-assemble__m[data-on] { opacity: 1; transform: none; }
.card-qr-assemble__finder {
position: absolute;
width: 35px;
height: 35px;
box-shadow: inset 0 0 0 5px light-dark(#373a40, #f1f2f3);
opacity: 0;
transform: scale(1.6) rotate(45deg);
transition: opacity 0.3s ease, transform 0.45s cubic-bezier(0.34, 1.5, 0.64, 1);
}
.card-qr-assemble__finder::after {
content: '';
position: absolute;
inset: 10px;
background: light-dark(#373a40, #f1f2f3);
}
.card-qr-assemble[data-ready] .card-qr-assemble__finder { opacity: 1; transform: none; transition-delay: 0.45s; }
.card-qr-assemble__label { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-qr-assemble__hint { color: light-dark(#5f626a, #a7abb2); font-size: 11px; margin-top: -8px; }
@media (prefers-reduced-motion: reduce) {
.card-qr-assemble__m, .card-qr-assemble__finder { transition: none; }
}
`;
const N = 21;
const inFinder = (x: number, y: number) =>
(x < 7 && y < 7) || (x >= N - 7 && y < 7) || (x < 7 && y >= N - 7);
export function QrAssembleCard() {
const [ready, setReady] = useState(false);
// Two seeded fields: the noise it shows at rest and the code it settles into.
const { noise, code, delay } = useMemo(() => {
let seed = 17;
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
const noise: boolean[] = [];
const code: boolean[] = [];
const delay: number[] = [];
for (let i = 0; i < N * N; i++) {
const x = i % N;
const y = Math.floor(i / N);
// Finder corners are drawn separately; keep a clear ring round them in the code.
const finderZone = (x < 8 && y < 8) || (x >= N - 8 && y < 8) || (x < 8 && y >= N - 8);
noise.push(!inFinder(x, y) && rand() > 0.62);
code.push(!finderZone && (rand() > 0.5 || (y === 6 && x % 2 === 0) || (x === 6 && y % 2 === 0)));
delay.push(rand() * 0.4);
}
return { noise, code, delay };
}, []);
const field = ready ? code : noise;
return (
<>
<style>
{css}
</style>
<div
className="card-qr-assemble"
data-ready={ready || undefined}
tabIndex={0}
role="group"
aria-label="Event ticket QR code"
onPointerEnter={() => setReady(true)}
onPointerLeave={() => setReady(false)}
onFocus={() => setReady(true)}
onBlur={() => setReady(false)}
>
<div className="card-qr-assemble__code" aria-hidden="true">
{field.map((on, i) => (
<span
key={i}
className="card-qr-assemble__m"
data-on={on || undefined}
style={{ '--d': `${delay[i].toFixed(2)}s` } as React.CSSProperties}
/>
))}
<span className="card-qr-assemble__finder" style={{ left: 6, top: 6 }} />
<span className="card-qr-assemble__finder" style={{ right: 6, top: 6 }} />
<span className="card-qr-assemble__finder" style={{ left: 6, bottom: 6 }} />
</div>
<span className="card-qr-assemble__label">{ready ? 'Ready to scan' : 'Generating…'}</span>
<span className="card-qr-assemble__hint">Gate B · Row 12</span>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardQrAssemble(el) on the mounted element instead.
<div class="card-qr-assemble" tabindex="0" role="group" aria-label="Event ticket QR code">
<div class="card-qr-assemble__code" aria-hidden="true">
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.40s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.23s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.00s"></span>
<span class="card-qr-assemble__m" style="--d:0.01s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.04s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.29s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.17s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.24s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.21s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.15s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.31s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.16s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.37s"></span>
<span class="card-qr-assemble__m" style="--d:0.19s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" style="--d:0.06s"></span>
<span class="card-qr-assemble__m" style="--d:0.35s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.39s"></span>
<span class="card-qr-assemble__m" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.32s"></span>
<span class="card-qr-assemble__m" style="--d:0.13s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.07s"></span>
<span class="card-qr-assemble__m" style="--d:0.33s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.26s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.30s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" style="--d:0.14s"></span>
<span class="card-qr-assemble__m" style="--d:0.34s"></span>
<span class="card-qr-assemble__m" style="--d:0.10s"></span>
<span class="card-qr-assemble__m" style="--d:0.18s"></span>
<span class="card-qr-assemble__m" style="--d:0.25s"></span>
<span class="card-qr-assemble__m" style="--d:0.05s"></span>
<span class="card-qr-assemble__m" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.03s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.11s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.22s"></span>
<span class="card-qr-assemble__m" style="--d:0.28s"></span>
<span class="card-qr-assemble__m" style="--d:0.12s"></span>
<span class="card-qr-assemble__m" style="--d:0.08s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.27s"></span>
<span class="card-qr-assemble__m" style="--d:0.36s"></span>
<span class="card-qr-assemble__m" data-on="true" style="--d:0.02s"></span>
<span class="card-qr-assemble__m" style="--d:0.09s"></span>
<span class="card-qr-assemble__m" style="--d:0.38s"></span>
<span class="card-qr-assemble__m" style="--d:0.20s"></span>
<span class="card-qr-assemble__finder" style="left:6px;top:6px"></span>
<span class="card-qr-assemble__finder" style="right:6px;top:6px"></span>
<span class="card-qr-assemble__finder" style="left:6px;bottom:6px"></span>
</div>
<span class="card-qr-assemble__label">Generating…</span>
<span class="card-qr-assemble__hint">Gate B · Row 12</span>
</div>.card-qr-assemble {
display: grid;
justify-items: center;
gap: 10px;
width: 176px;
padding: 14px 14px 12px;
border-radius: 16px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
cursor: pointer;
}
.card-qr-assemble:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-qr-assemble__code {
position: relative;
display: grid;
grid-template-columns: repeat(21, 5px);
grid-auto-rows: 5px;
padding: 6px;
border-radius: 8px;
background: light-dark(#fbfcfd, #121417);
}
.card-qr-assemble__m {
background: light-dark(#373a40, #f1f2f3);
opacity: 0;
transform: scale(0.4);
transition: opacity 0.3s ease var(--d), transform 0.3s cubic-bezier(0.34, 1.5, 0.64, 1) var(--d);
}
.card-qr-assemble__m[data-on] { opacity: 1; transform: none; }
.card-qr-assemble__finder {
position: absolute;
width: 35px;
height: 35px;
box-shadow: inset 0 0 0 5px light-dark(#373a40, #f1f2f3);
opacity: 0;
transform: scale(1.6) rotate(45deg);
transition: opacity 0.3s ease, transform 0.45s cubic-bezier(0.34, 1.5, 0.64, 1);
}
.card-qr-assemble__finder::after {
content: '';
position: absolute;
inset: 10px;
background: light-dark(#373a40, #f1f2f3);
}
.card-qr-assemble[data-ready] .card-qr-assemble__finder { opacity: 1; transform: none; transition-delay: 0.45s; }
.card-qr-assemble__label { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-qr-assemble__hint { color: light-dark(#5f626a, #a7abb2); font-size: 11px; margin-top: -8px; }
@media (prefers-reduced-motion: reduce) {
.card-qr-assemble__m, .card-qr-assemble__finder { transition: none; }
}function initCardQrAssemble(root) {
var mods = root.querySelectorAll('.card-qr-assemble__m');
var label = root.querySelector('.card-qr-assemble__label');
var N = 21;
if (mods.length !== N * N || !label) return;
// Two seeded fields (the same as the markup was built from): the noise it shows at rest and the code it settles into.
var seed = 17;
function rand() { return (seed = (seed * 16807) % 2147483647) / 2147483647; }
function inFinder(x, y) { return (x < 7 && y < 7) || (x >= N - 7 && y < 7) || (x < 7 && y >= N - 7); }
var noise = [];
var code = [];
for (var i = 0; i < N * N; i++) {
var x = i % N;
var y = Math.floor(i / N);
var zone = (x < 8 && y < 8) || (x >= N - 8 && y < 8) || (x < 8 && y >= N - 8);
noise.push(!inFinder(x, y) && rand() > 0.62);
code.push(!zone && (rand() > 0.5 || (y === 6 && x % 2 === 0) || (x === 6 && y % 2 === 0)));
rand(); // the delay, already in the markup
}
function set(ready) {
root.toggleAttribute('data-ready', ready);
var field = ready ? code : noise;
mods.forEach(function (m, k) { m.toggleAttribute('data-on', field[k]); });
label.textContent = ready ? 'Ready to scan' : 'Generating…';
}
function on() { set(true); }
function off() { set(false); }
root.addEventListener('pointerenter', on);
root.addEventListener('pointerleave', off);
root.addEventListener('focus', on);
root.addEventListener('blur', off);
return function () {
root.removeEventListener('pointerenter', on);
root.removeEventListener('pointerleave', off);
root.removeEventListener('focus', on);
root.removeEventListener('blur', off);
};
}
document.querySelectorAll('.card-qr-assemble').forEach(initCardQrAssemble);Goal: Create a "QR Assemble" card.
Visual behavior: A ticket card whose QR code assembles itself: at rest it shows random noise, and on hover the modules sort themselves into a code with staggered pops, then the three finder squares spin in and lock into the corners.
Technique:
- A 21 x 21 grid of modules; two seeded random fields decide which modules are on at rest (noise) and when ready (the code, with a clear zone and timing lines round the finders).
- Each module transitions opacity and scale with its own random delay up to 0.4s, so the code settles in a shimmer rather than all at once.
- The finder squares are separate elements (an inset box-shadow ring plus a ::after centre) that fly in from scale 1.6 and 45deg after the modules.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The label switches from Generating to Ready to scan. Under prefers-reduced-motion the code appears at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "QR Assemble" card.
Visual behavior: A ticket card whose QR code assembles itself: at rest it shows random noise, and on hover the modules sort themselves into a code with staggered pops, then the three finder squares spin in and lock into the corners.
Technique:
- A 21 x 21 grid of modules; two seeded random fields decide which modules are on at rest (noise) and when ready (the code, with a clear zone and timing lines round the finders).
- Each module transitions opacity and scale with its own random delay up to 0.4s, so the code settles in a shimmer rather than all at once.
- The finder squares are separate elements (an inset box-shadow ring plus a ::after centre) that fly in from scale 1.6 and 45deg after the modules.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The label switches from Generating to Ready to scan. Under prefers-reduced-motion the code appears at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A ticket card whose QR code assembles itself: at rest it shows random noise, and on hover the modules sort themselves into a code with staggered pops, then the three finder squares spin in and lock into the corners.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, state change, delay stagger, card
- Techniques
- css transition, css variables, transition delay stagger, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The label switches from Generating to Ready to scan. Under prefers-reduced-motion the code appears at once.