Shatter
import { useState } from 'react';
const css = `
@keyframes card-shatter-crack { 0% { opacity: 1; } 100% { opacity: 0; } }
@keyframes card-shatter-flash { 0% { opacity: 0.9; transform: scale(0.2); } 100% { opacity: 0; transform: scale(1.6); } }
.card-shatter {
position: relative;
display: block;
width: 224px;
height: 148px;
padding: 0;
border: 0;
background: none;
font: inherit;
text-align: left;
perspective: 600px;
cursor: pointer;
}
.card-shatter:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 4px; border-radius: 16px; }
.card-shatter__prize {
position: absolute;
inset: 0;
display: grid;
align-content: center;
justify-items: center;
gap: 4px;
border-radius: 16px;
background: radial-gradient(circle at 50% 40%, light-dark(#dbdde1, #2e3035), light-dark(#fbfcfd, #121417) 75%);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
transform: scale(0.88);
opacity: 0.3;
transition: transform 0.5s cubic-bezier(0.34, 1.5, 0.64, 1), opacity 0.3s ease;
}
.card-shatter[aria-pressed='true'] .card-shatter__prize { transform: none; opacity: 1; transition-delay: 0.18s; }
.card-shatter__big { color: light-dark(#373a40, #f1f2f3); font-size: 34px; font-weight: 600; letter-spacing: -0.04em; line-height: 1; }
.card-shatter__sub { color: light-dark(#50535b, #c6c8cd); font-size: 11px; }
.card-shatter__code {
margin-top: 6px;
padding: 3px 10px;
border-radius: 6px;
border: 1px dashed light-dark(#5f626a, #a7abb2);
color: light-dark(#373a40, #f1f2f3);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
letter-spacing: 0.12em;
}
.card-shatter__hint { position: absolute; bottom: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; }
.card-shatter__cover {
position: absolute;
inset: 0;
display: grid;
align-content: end;
padding: 14px;
border-radius: 16px;
background:
radial-gradient(circle at 168px 52px, light-dark(#dbdde1, #2e3035) 0 26px, transparent 27px),
repeating-linear-gradient(-45deg, light-dark(#ebedf0, #232529) 0 6px, color-mix(in oklab, light-dark(#dbdde1, #2e3035) 55%, light-dark(#ebedf0, #232529)) 6px 12px);
box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41);
}
.card-shatter__seal {
position: absolute;
left: 142px;
top: 26px;
width: 52px;
height: 52px;
border-radius: 50%;
border: 1.5px dashed light-dark(#5f626a, #a7abb2);
}
.card-shatter__seal svg { position: absolute; inset: 0; width: 52px; height: 52px; color: light-dark(#42454c, #e5e6e9); }
.card-shatter__cover b { color: light-dark(#373a40, #f1f2f3); font-size: 15px; font-weight: 600; }
.card-shatter__cover span { color: light-dark(#50535b, #c6c8cd); font-size: 11px; }
.card-shatter__whole { position: absolute; inset: 0; z-index: 2; transition: opacity 0s linear 0.8s; }
.card-shatter[aria-pressed='true'] .card-shatter__whole { opacity: 0; transition-delay: 0s; }
.card-shatter__shard {
position: absolute;
inset: 0;
z-index: 1;
transform: translate3d(var(--tx, 0px), var(--ty, 0px), var(--tz, 0px)) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) rotateZ(var(--rz, 0deg));
transition: transform 0.6s cubic-bezier(0.5, 0, 0.2, 1) var(--d, 0s), opacity 0.2s ease var(--d, 0s);
}
.card-shatter[aria-pressed='true'] .card-shatter__shard {
opacity: 0;
transition: transform 0.85s cubic-bezier(0.15, 0.6, 0.35, 1) var(--d, 0s), opacity 0.4s ease calc(var(--d, 0s) + 0.4s);
}
.card-shatter__cracks { position: absolute; inset: 0; z-index: 3; width: 224px; height: 148px; opacity: 0; pointer-events: none; }
.card-shatter__cracks path { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.2; stroke-linejoin: round; }
.card-shatter[aria-pressed='true'] .card-shatter__cracks { animation: card-shatter-crack 0.5s ease-out; }
.card-shatter__flash {
position: absolute;
z-index: 3;
width: 90px;
height: 90px;
margin: -45px 0 0 -45px;
border-radius: 50%;
background: radial-gradient(circle, light-dark(#373a40, #f1f2f3), transparent 65%);
opacity: 0;
pointer-events: none;
}
.card-shatter[aria-pressed='true'] .card-shatter__flash { animation: card-shatter-flash 0.45s ease-out; }
@media (prefers-reduced-motion: reduce) {
.card-shatter__shard, .card-shatter[aria-pressed='true'] .card-shatter__shard { transform: none; transition: opacity 0.3s ease; }
.card-shatter[aria-pressed='true'] .card-shatter__cracks, .card-shatter[aria-pressed='true'] .card-shatter__flash { animation: none; }
}
`;
const W = 224;
const H = 148;
const COLS = 4;
const ROWS = 3;
// A fixed pseudo-random sequence, so the crack pattern is the same on every load.
const rnd = (n: number) => {
const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
};
// A jittered grid of points: interior points wander, edge points stay on their edge.
const PTS = Array.from({ length: ROWS + 1 }, (_, r) =>
Array.from({ length: COLS + 1 }, (_, c) => [
(c / COLS) * W + (c === 0 || c === COLS ? 0 : (rnd(r * 9 + c) - 0.5) * 34),
(r / ROWS) * H + (r === 0 || r === ROWS ? 0 : (rnd(r * 9 + c + 50) - 0.5) * 28),
]),
);
// Each cell splits into two triangles, alternating the diagonal so the cracks don't line up.
const SHARDS = PTS.slice(0, ROWS).flatMap((row, r) =>
row.slice(0, COLS).flatMap((_, c) => {
const [a, b, d, e] = [PTS[r][c], PTS[r][c + 1], PTS[r + 1][c], PTS[r + 1][c + 1]];
const tris = (r + c) % 2 ? [[a, b, e], [a, e, d]] : [[a, b, d], [b, e, d]];
return tris.map((p) => ({ p, cx: (p[0][0] + p[1][0] + p[2][0]) / 3, cy: (p[0][1] + p[1][1] + p[2][1]) / 3 }));
}),
);
const CRACKS = SHARDS.map(({ p }) => `M${p.map((q) => q.map((n) => n.toFixed(1)).join(' ')).join('L')}Z`).join('');
function Cover() {
return (
<span className="card-shatter__cover">
<span className="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor" />
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
);
}
export function ShatterCard() {
const [broken, setBroken] = useState(false);
const [hit, setHit] = useState({ x: W / 2, y: H / 2 });
const toggle = (x: number, y: number) => {
if (!broken) setHit({ x, y });
setBroken(!broken);
};
return (
<>
<style>
{css}
</style>
<button
type="button"
className="card-shatter"
aria-pressed={broken}
aria-label={broken ? 'Reward revealed: 30% off with code MOVIN30. Press to reseal' : 'Mystery drop. Press to break the seal'}
onClick={(e) => {
const r = e.currentTarget.getBoundingClientRect();
// Keyboard presses report 0,0: break from the middle instead.
const fromKey = e.detail === 0;
toggle(fromKey ? W / 2 : e.clientX - r.left, fromKey ? H / 2 : e.clientY - r.top);
}}
>
<span className="card-shatter__prize" aria-hidden="true">
<span className="card-shatter__big">30% off</span>
<span className="card-shatter__sub">your next order</span>
<span className="card-shatter__code">MOVIN30</span>
<span className="card-shatter__hint">Click to reseal</span>
</span>
{SHARDS.map(({ p, cx, cy }, i) => {
const dx = cx - hit.x;
const dy = cy - hit.y;
const dist = Math.hypot(dx, dy) || 1;
const push = 50 + rnd(i + 7) * 80;
const vars = broken
? {
'--tx': `${((dx / dist) * push).toFixed(1)}px`,
'--ty': `${((dy / dist) * push + 60 + rnd(i + 3) * 40).toFixed(1)}px`,
'--tz': `${(40 + rnd(i + 11) * 80).toFixed(0)}px`,
'--rx': `${((rnd(i + 13) - 0.5) * 140).toFixed(0)}deg`,
'--ry': `${((rnd(i + 17) - 0.5) * 140).toFixed(0)}deg`,
'--rz': `${((rnd(i + 19) - 0.5) * 180).toFixed(0)}deg`,
'--d': `${(0.08 + dist * 0.0016).toFixed(3)}s`,
}
: { '--d': `${(rnd(i + 23) * 0.12).toFixed(3)}s` };
return (
<span
key={i}
className="card-shatter__shard"
aria-hidden="true"
style={{
clipPath: `polygon(${p.map((q) => `${q[0].toFixed(1)}px ${q[1].toFixed(1)}px`).join(',')})`,
transformOrigin: `${cx.toFixed(1)}px ${cy.toFixed(1)}px`,
...(vars as React.CSSProperties),
}}
>
<Cover />
</span>
);
})}
<span className="card-shatter__whole" aria-hidden="true">
<Cover />
</span>
<svg className="card-shatter__cracks" viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
<path d={CRACKS} />
</svg>
<span className="card-shatter__flash" aria-hidden="true" style={{ left: hit.x, top: hit.y }} />
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardShatter(el) on the mounted element instead.
<button type="button" class="card-shatter" aria-pressed="false" aria-label="Mystery drop. Press to break the seal">
<span class="card-shatter__prize" aria-hidden="true">
<span class="card-shatter__big">30% off</span>
<span class="card-shatter__sub">your next order</span>
<span class="card-shatter__code">MOVIN30</span>
<span class="card-shatter__hint">Click to reseal</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(0.0px 0.0px,42.6px 0.0px,0.0px 59.9px);transform-origin:14.2px 20.0px;--d:0.102s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(42.6px 0.0px,64.5px 50.2px,0.0px 59.9px);transform-origin:35.7px 36.7px;--d:0.027s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(42.6px 0.0px,122.2px 0.0px,117.1px 55.5px);transform-origin:93.9px 18.5px;--d:0.064s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(42.6px 0.0px,117.1px 55.5px,64.5px 50.2px);transform-origin:74.7px 35.2px;--d:0.070s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(122.2px 0.0px,172.4px 0.0px,117.1px 55.5px);transform-origin:137.2px 18.5px;--d:0.059s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(172.4px 0.0px,158.0px 39.8px,117.1px 55.5px);transform-origin:149.2px 31.8px;--d:0.006s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(172.4px 0.0px,224.0px 0.0px,224.0px 58.4px);transform-origin:206.8px 19.5px;--d:0.007s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(172.4px 0.0px,224.0px 58.4px,158.0px 39.8px);transform-origin:184.8px 32.7px;--d:0.035s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(0.0px 59.9px,64.5px 50.2px,45.8px 99.1px);transform-origin:36.8px 69.7px;--d:0.076s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(0.0px 59.9px,45.8px 99.1px,0.0px 88.5px);transform-origin:15.3px 82.5px;--d:0.005s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(64.5px 50.2px,117.1px 55.5px,45.8px 99.1px);transform-origin:75.8px 68.3px;--d:0.043s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(117.1px 55.5px,119.8px 109.8px,45.8px 99.1px);transform-origin:94.2px 88.2px;--d:0.089s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(117.1px 55.5px,158.0px 39.8px,154.6px 105.1px);transform-origin:143.2px 66.8px;--d:0.005s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(117.1px 55.5px,154.6px 105.1px,119.8px 109.8px);transform-origin:130.5px 90.2px;--d:0.119s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(158.0px 39.8px,224.0px 58.4px,154.6px 105.1px);transform-origin:178.9px 67.8px;--d:0.006s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(224.0px 58.4px,224.0px 109.7px,154.6px 105.1px);transform-origin:200.9px 91.1px;--d:0.037s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(0.0px 88.5px,45.8px 99.1px,0.0px 148.0px);transform-origin:15.3px 111.9px;--d:0.106s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(45.8px 99.1px,40.8px 148.0px,0.0px 148.0px);transform-origin:28.9px 131.7px;--d:0.016s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(45.8px 99.1px,119.8px 109.8px,96.9px 148.0px);transform-origin:87.5px 119.0px;--d:0.068s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(45.8px 99.1px,96.9px 148.0px,40.8px 148.0px);transform-origin:61.2px 131.7px;--d:0.035s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(119.8px 109.8px,154.6px 105.1px,96.9px 148.0px);transform-origin:123.8px 121.0px;--d:0.067s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(154.6px 105.1px,160.9px 148.0px,96.9px 148.0px);transform-origin:137.5px 133.7px;--d:0.057s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(154.6px 105.1px,224.0px 109.7px,224.0px 148.0px);transform-origin:200.9px 120.9px;--d:0.087s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__shard" aria-hidden="true" style="clip-path:polygon(154.6px 105.1px,224.0px 148.0px,160.9px 148.0px);transform-origin:179.8px 133.7px;--d:0.077s">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<span class="card-shatter__whole" aria-hidden="true">
<span class="card-shatter__cover">
<span class="card-shatter__seal">
<svg viewBox="0 0 52 52" fill="none">
<path d="M26 14l3.4 7.6 8.2.8-6.2 5.5 1.8 8.1L26 31.8 18.8 36l1.8-8.1-6.2-5.5 8.2-.8z" fill="currentColor"></path>
</svg>
</span>
<b>Mystery drop</b>
<span>Click to break the seal</span>
</span>
</span>
<svg class="card-shatter__cracks" viewBox="0 0 224 148" aria-hidden="true">
<path d="M0.0 0.0L42.6 0.0L0.0 59.9ZM42.6 0.0L64.5 50.2L0.0 59.9ZM42.6 0.0L122.2 0.0L117.1 55.5ZM42.6 0.0L117.1 55.5L64.5 50.2ZM122.2 0.0L172.4 0.0L117.1 55.5ZM172.4 0.0L158.0 39.8L117.1 55.5ZM172.4 0.0L224.0 0.0L224.0 58.4ZM172.4 0.0L224.0 58.4L158.0 39.8ZM0.0 59.9L64.5 50.2L45.8 99.1ZM0.0 59.9L45.8 99.1L0.0 88.5ZM64.5 50.2L117.1 55.5L45.8 99.1ZM117.1 55.5L119.8 109.8L45.8 99.1ZM117.1 55.5L158.0 39.8L154.6 105.1ZM117.1 55.5L154.6 105.1L119.8 109.8ZM158.0 39.8L224.0 58.4L154.6 105.1ZM224.0 58.4L224.0 109.7L154.6 105.1ZM0.0 88.5L45.8 99.1L0.0 148.0ZM45.8 99.1L40.8 148.0L0.0 148.0ZM45.8 99.1L119.8 109.8L96.9 148.0ZM45.8 99.1L96.9 148.0L40.8 148.0ZM119.8 109.8L154.6 105.1L96.9 148.0ZM154.6 105.1L160.9 148.0L96.9 148.0ZM154.6 105.1L224.0 109.7L224.0 148.0ZM154.6 105.1L224.0 148.0L160.9 148.0Z"></path>
</svg>
<span class="card-shatter__flash" aria-hidden="true" style="left:112px;top:74px"></span>
</button>@keyframes card-shatter-crack { 0% { opacity: 1; } 100% { opacity: 0; } }
@keyframes card-shatter-flash { 0% { opacity: 0.9; transform: scale(0.2); } 100% { opacity: 0; transform: scale(1.6); } }
.card-shatter {
position: relative;
display: block;
width: 224px;
height: 148px;
padding: 0;
border: 0;
background: none;
font: inherit;
text-align: left;
perspective: 600px;
cursor: pointer;
}
.card-shatter:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 4px; border-radius: 16px; }
.card-shatter__prize {
position: absolute;
inset: 0;
display: grid;
align-content: center;
justify-items: center;
gap: 4px;
border-radius: 16px;
background: radial-gradient(circle at 50% 40%, light-dark(#dbdde1, #2e3035), light-dark(#fbfcfd, #121417) 75%);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
transform: scale(0.88);
opacity: 0.3;
transition: transform 0.5s cubic-bezier(0.34, 1.5, 0.64, 1), opacity 0.3s ease;
}
.card-shatter[aria-pressed='true'] .card-shatter__prize { transform: none; opacity: 1; transition-delay: 0.18s; }
.card-shatter__big { color: light-dark(#373a40, #f1f2f3); font-size: 34px; font-weight: 600; letter-spacing: -0.04em; line-height: 1; }
.card-shatter__sub { color: light-dark(#50535b, #c6c8cd); font-size: 11px; }
.card-shatter__code {
margin-top: 6px;
padding: 3px 10px;
border-radius: 6px;
border: 1px dashed light-dark(#5f626a, #a7abb2);
color: light-dark(#373a40, #f1f2f3);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
letter-spacing: 0.12em;
}
.card-shatter__hint { position: absolute; bottom: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; }
.card-shatter__cover {
position: absolute;
inset: 0;
display: grid;
align-content: end;
padding: 14px;
border-radius: 16px;
background:
radial-gradient(circle at 168px 52px, light-dark(#dbdde1, #2e3035) 0 26px, transparent 27px),
repeating-linear-gradient(-45deg, light-dark(#ebedf0, #232529) 0 6px, color-mix(in oklab, light-dark(#dbdde1, #2e3035) 55%, light-dark(#ebedf0, #232529)) 6px 12px);
box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41);
}
.card-shatter__seal {
position: absolute;
left: 142px;
top: 26px;
width: 52px;
height: 52px;
border-radius: 50%;
border: 1.5px dashed light-dark(#5f626a, #a7abb2);
}
.card-shatter__seal svg { position: absolute; inset: 0; width: 52px; height: 52px; color: light-dark(#42454c, #e5e6e9); }
.card-shatter__cover b { color: light-dark(#373a40, #f1f2f3); font-size: 15px; font-weight: 600; }
.card-shatter__cover span { color: light-dark(#50535b, #c6c8cd); font-size: 11px; }
.card-shatter__whole { position: absolute; inset: 0; z-index: 2; transition: opacity 0s linear 0.8s; }
.card-shatter[aria-pressed='true'] .card-shatter__whole { opacity: 0; transition-delay: 0s; }
.card-shatter__shard {
position: absolute;
inset: 0;
z-index: 1;
transform: translate3d(var(--tx, 0px), var(--ty, 0px), var(--tz, 0px)) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) rotateZ(var(--rz, 0deg));
transition: transform 0.6s cubic-bezier(0.5, 0, 0.2, 1) var(--d, 0s), opacity 0.2s ease var(--d, 0s);
}
.card-shatter[aria-pressed='true'] .card-shatter__shard {
opacity: 0;
transition: transform 0.85s cubic-bezier(0.15, 0.6, 0.35, 1) var(--d, 0s), opacity 0.4s ease calc(var(--d, 0s) + 0.4s);
}
.card-shatter__cracks { position: absolute; inset: 0; z-index: 3; width: 224px; height: 148px; opacity: 0; pointer-events: none; }
.card-shatter__cracks path { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.2; stroke-linejoin: round; }
.card-shatter[aria-pressed='true'] .card-shatter__cracks { animation: card-shatter-crack 0.5s ease-out; }
.card-shatter__flash {
position: absolute;
z-index: 3;
width: 90px;
height: 90px;
margin: -45px 0 0 -45px;
border-radius: 50%;
background: radial-gradient(circle, light-dark(#373a40, #f1f2f3), transparent 65%);
opacity: 0;
pointer-events: none;
}
.card-shatter[aria-pressed='true'] .card-shatter__flash { animation: card-shatter-flash 0.45s ease-out; }
@media (prefers-reduced-motion: reduce) {
.card-shatter__shard, .card-shatter[aria-pressed='true'] .card-shatter__shard { transform: none; transition: opacity 0.3s ease; }
.card-shatter[aria-pressed='true'] .card-shatter__cracks, .card-shatter[aria-pressed='true'] .card-shatter__flash { animation: none; }
}function initCardShatter(root) {
var shards = root.querySelectorAll('.card-shatter__shard');
var flash = root.querySelector('.card-shatter__flash');
if (!shards.length || !flash) return;
var W = 224;
var H = 148;
// The same fixed pseudo-random sequence the shard geometry was built from.
function rnd(n) {
var x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
return x - Math.floor(x);
}
// Each shard's centre is its transform-origin.
var centres = Array.prototype.map.call(shards, function (s) {
var o = s.style.transformOrigin.split(' ');
return { x: parseFloat(o[0]), y: parseFloat(o[1]) };
});
var broken = false;
function click(e) {
var r = root.getBoundingClientRect();
// Keyboard presses report no position: break from the middle.
var hx = e.detail === 0 ? W / 2 : e.clientX - r.left;
var hy = e.detail === 0 ? H / 2 : e.clientY - r.top;
broken = !broken;
if (broken) {
flash.style.left = hx + 'px';
flash.style.top = hy + 'px';
}
shards.forEach(function (s, i) {
var c = centres[i];
if (broken) {
var dx = c.x - hx;
var dy = c.y - hy;
var dist = Math.hypot(dx, dy) || 1;
var push = 50 + rnd(i + 7) * 80;
s.style.setProperty('--tx', ((dx / dist) * push).toFixed(1) + 'px');
s.style.setProperty('--ty', ((dy / dist) * push + 60 + rnd(i + 3) * 40).toFixed(1) + 'px');
s.style.setProperty('--tz', (40 + rnd(i + 11) * 80).toFixed(0) + 'px');
s.style.setProperty('--rx', ((rnd(i + 13) - 0.5) * 140).toFixed(0) + 'deg');
s.style.setProperty('--ry', ((rnd(i + 17) - 0.5) * 140).toFixed(0) + 'deg');
s.style.setProperty('--rz', ((rnd(i + 19) - 0.5) * 180).toFixed(0) + 'deg');
s.style.setProperty('--d', (0.08 + dist * 0.0016).toFixed(3) + 's');
} else {
['--tx', '--ty', '--tz', '--rx', '--ry', '--rz'].forEach(function (k) { s.style.removeProperty(k); });
s.style.setProperty('--d', (rnd(i + 23) * 0.12).toFixed(3) + 's');
}
});
root.setAttribute('aria-pressed', String(broken));
root.setAttribute('aria-label', broken ? 'Reward revealed: 30% off with code MOVIN30. Press to reseal' : 'Mystery drop. Press to break the seal');
}
root.addEventListener('click', click);
return function () { root.removeEventListener('click', click); };
}
document.querySelectorAll('.card-shatter').forEach(initCardShatter);Goal: Create a "Shatter" card.
Visual behavior: A sealed mystery card. Click and it cracks from exactly where you clicked into 3D shards that fly apart and fall away, revealing a reward underneath; click again and the shards fly back and reassemble.
Technique:
- A jittered 4 x 3 point grid is split into 24 triangles (alternating diagonals); each shard is a copy of the cover clipped with clip-path: polygon() to one triangle.
- On click each shard gets CSS variables computed from the click point: a push along the direction away from it, a downward fall, a depth and random rotations on three axes, plus a delay that grows with distance, so the crack spreads outward.
- A single whole cover hides the hairline seams between shards at rest and is swapped out the instant it breaks; the crack lines flash and a burst marks the impact.
- Clicking again clears the variables, so the same transition runs every shard back into place.
Accessibility: A real <button> with aria-pressed; its aria-label says what is revealed. Keyboard presses break it from the centre. Under prefers-reduced-motion the shards fade instead of flying.
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 "Shatter" card.
Visual behavior: A sealed mystery card. Click and it cracks from exactly where you clicked into 3D shards that fly apart and fall away, revealing a reward underneath; click again and the shards fly back and reassemble.
Technique:
- A jittered 4 x 3 point grid is split into 24 triangles (alternating diagonals); each shard is a copy of the cover clipped with clip-path: polygon() to one triangle.
- On click each shard gets CSS variables computed from the click point: a push along the direction away from it, a downward fall, a depth and random rotations on three axes, plus a delay that grows with distance, so the crack spreads outward.
- A single whole cover hides the hairline seams between shards at rest and is swapped out the instant it breaks; the crack lines flash and a burst marks the impact.
- Clicking again clears the variables, so the same transition runs every shard back into place.
Accessibility: A real <button> with aria-pressed; its aria-label says what is revealed. Keyboard presses break it from the centre. Under prefers-reduced-motion the shards fade instead of flying.
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 sealed mystery card. Click and it cracks from exactly where you clicked into 3D shards that fly apart and fall away, revealing a reward underneath; click again and the shards fly back and reassemble.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- click, 3d, state change, card
- Techniques
- clip path, 3d transform, css variables, transition delay stagger, event position
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with aria-pressed; its aria-label says what is revealed. Keyboard presses break it from the centre. Under prefers-reduced-motion the shards fade instead of flying.