Clapper
React + Tailwind
import { useEffect, useRef, useState, type CSSProperties } from 'react';
type Phase = 'slate' | 'clap' | 'drop' | 'leader' | 'film' | 'iris' | 'end';
const DUST = [
[-30, -18],
[-16, -30],
[4, -26],
[22, -32],
[36, -14],
[-40, -6],
[46, -4],
];
const css = `
.card-clapper { flex: none; --slate: light-dark(#42454c, #232529); --chalk: light-dark(#fbfcfd, #f1f2f3); --stick: light-dark(#373a40, #121417); --film: light-dark(#6d7179, #7d8087); position: relative; width: 248px; height: 220px; color: var(--chalk); }
.card-clapper__screen { position: absolute; left: 0; right: 0; bottom: 8px; height: 146px; overflow: hidden; border-radius: 4px; background: var(--film); box-shadow: 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.card-clapper__weave { position: absolute; inset: 0; }
.card-clapper[data-phase='leader'] .card-clapper__weave, .card-clapper[data-phase='film'] .card-clapper__weave, .card-clapper[data-phase='iris'] .card-clapper__weave { animation: card-clapper-weave 0.24s steps(3) infinite; }
@keyframes card-clapper-weave {
0% { transform: translate(0, 0); filter: brightness(1); }
33% { transform: translate(0.6px, -0.8px); filter: brightness(1.1); }
66% { transform: translate(-0.5px, 0.5px); filter: brightness(0.92); }
}
.card-clapper__leader { position: absolute; inset: 0; display: grid; place-items: center; background: var(--film); }
.card-clapper__leader svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.card-clapper__cross { stroke: var(--stick); stroke-width: 1; }
.card-clapper__circle { fill: none; stroke: var(--chalk); stroke-width: 2; }
.card-clapper__wipe { fill: none; stroke: var(--stick); stroke-opacity: 0.38; stroke-width: 58; stroke-dasharray: 1; transform: rotate(-90deg); transform-origin: 124px 73px; animation: card-clapper-wipe 0.8s linear infinite; }
@keyframes card-clapper-wipe {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
.card-clapper__num { position: relative; font: 700 62px/1 Georgia, 'Times New Roman', serif; color: var(--chalk); }
.card-clapper__reel { position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 46%, color-mix(in oklab, var(--chalk) 74%, var(--film)), var(--film) 92%); }
.card-clapper__floor { position: absolute; left: 0; right: 0; bottom: 0; height: 32px; background: linear-gradient(to bottom, var(--stick), color-mix(in oklab, var(--stick) 70%, var(--film))); }
.card-clapper__ball { position: absolute; left: 50%; bottom: 32px; width: 30px; height: 30px; margin-left: -15px; border-radius: 50%; background: var(--stick); transform-origin: 50% 100%; animation: card-clapper-bounce 0.92s linear infinite; }
@keyframes card-clapper-bounce {
0% { transform: translateY(0) scale(1.34, 0.66); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
12% { transform: translateY(-10px) scale(0.88, 1.16); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
50% { transform: translateY(-74px) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
90% { transform: translateY(-10px) scale(0.88, 1.16); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
100% { transform: translateY(0) scale(1.34, 0.66); }
}
.card-clapper__ballshadow { position: absolute; left: 50%; bottom: 14px; width: 44px; height: 8px; margin-left: -22px; border-radius: 50%; background: var(--stick); opacity: 0.6; animation: card-clapper-pool 0.92s linear infinite; }
@keyframes card-clapper-pool {
0%, 100% { transform: scale(1); opacity: 1; animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
50% { transform: scale(0.42); opacity: 0.42; animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
}
.card-clapper__scratch { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--chalk); opacity: 0.5; animation: card-clapper-scratch 0.6s steps(5) infinite; }
@keyframes card-clapper-scratch {
0% { left: 18%; opacity: 0.5; }
25% { left: 73%; opacity: 0.2; }
50% { left: 41%; opacity: 0.55; }
75% { left: 88%; opacity: 0.12; }
100% { left: 26%; opacity: 0.4; }
}
.card-clapper__iris { position: absolute; inset: -60px; pointer-events: none; background: radial-gradient(circle at 50% 50%, transparent var(--card-clapper-open), var(--stick) calc(var(--card-clapper-open) + 1px)); }
.card-clapper[data-phase='iris'] .card-clapper__iris { animation: card-clapper-iris 0.85s ease-in forwards; }
@property --card-clapper-open { syntax: '<length>'; inherits: false; initial-value: 190px; }
@keyframes card-clapper-iris {
from { --card-clapper-open: 190px; }
to { --card-clapper-open: 0px; }
}
.card-clapper__end { position: absolute; inset: 0; display: grid; place-items: center; background: var(--stick); color: var(--chalk); font: italic 700 25px/1 Georgia, 'Times New Roman', serif; animation: card-clapper-end 0.3s ease both; }
@keyframes card-clapper-end {
from { opacity: 0; }
}
.card-clapper__press { position: absolute; left: 0; right: 0; bottom: 8px; height: 178px; padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; transform-origin: 50% 100%; transition: transform 0.42s cubic-bezier(0.55, 0, 0.9, 0.5), opacity 0.42s; }
.card-clapper__press:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 6px; }
.card-clapper[data-phase='drop'] .card-clapper__press, .card-clapper[data-phase='leader'] .card-clapper__press, .card-clapper[data-phase='film'] .card-clapper__press, .card-clapper[data-phase='iris'] .card-clapper__press, .card-clapper[data-phase='end'] .card-clapper__press { transform: translateY(250px) rotate(6deg); }
.card-clapper[data-phase='clap'] .card-clapper__press { animation: card-clapper-jolt 0.32s cubic-bezier(0.3, 1.6, 0.5, 1) 0.14s; }
@keyframes card-clapper-jolt {
0% { transform: translateY(3px) scale(1.012, 0.985); }
100% { transform: none; }
}
.card-clapper__stick { position: absolute; left: 0; right: 0; height: 20px; background: repeating-linear-gradient(-60deg, var(--stick) 0 15px, var(--chalk) 15px 30px); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)); }
.card-clapper__stick--top { top: 0; border-radius: 3px 3px 0 0; transform-origin: 9px 100%; transform: rotate(-8.5deg); transition: transform 0.34s cubic-bezier(0.3, 1.4, 0.5, 1); background: repeating-linear-gradient(60deg, var(--stick) 0 15px, var(--chalk) 15px 30px); }
.card-clapper__press:hover .card-clapper__stick--top { transform: rotate(-10.5deg); }
.card-clapper[data-phase='clap'] .card-clapper__stick--top, .card-clapper[data-phase='drop'] .card-clapper__stick--top { transform: rotate(0); transition: transform 0.13s cubic-bezier(0.7, 0, 1, 0.5); }
.card-clapper__stick--low { top: 20px; }
.card-clapper__hinge { position: absolute; left: 4px; top: 14px; width: 12px; height: 12px; border-radius: 50%; background: var(--film); box-shadow: 0 0 0 1.5px var(--stick); }
.card-clapper__board { position: absolute; left: 0; right: 0; top: 40px; bottom: 0; display: grid; grid-template-rows: 1fr 1.5fr 1fr; border-radius: 0 0 5px 5px; background: var(--slate); overflow: hidden; box-shadow: 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.card-clapper__row { display: grid; border-top: 1.5px solid color-mix(in oklab, var(--chalk) 66%, transparent); }
.card-clapper__row:first-child { border-top: 0; }
.card-clapper__row--three { grid-template-columns: 1.1fr 1fr 1fr; }
.card-clapper__row--two { grid-template-columns: 1fr 1fr; }
.card-clapper__cell { min-width: 0; padding: 3px 8px; border-left: 1.5px solid color-mix(in oklab, var(--chalk) 66%, transparent); }
.card-clapper__cell:first-child { border-left: 0; }
.card-clapper__label { display: block; font: 700 7.5px/1.1 system-ui, sans-serif; letter-spacing: 0.1em; text-transform: uppercase; }
.card-clapper__value { display: block; overflow: hidden; font-family: 'Chalkboard SE', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive; font-size: 13.5px; line-height: 1.2; white-space: nowrap; }
.card-clapper__value--big { font-size: 25px; line-height: 1.08; }
.card-clapper__take { display: block; animation: card-clapper-take 0.3s cubic-bezier(0.3, 1.7, 0.5, 1); }
@keyframes card-clapper-take {
from { transform: scale(1.6); opacity: 0; }
}
.card-clapper__dust { position: absolute; left: 60%; top: 28px; width: 0; height: 0; pointer-events: none; }
.card-clapper__dust i { position: absolute; width: var(--s); height: var(--s); border-radius: 50%; background: var(--chalk); animation: card-clapper-dust 0.62s ease-out forwards; }
@keyframes card-clapper-dust {
from { opacity: 0.9; transform: none; }
to { opacity: 0; transform: translate(var(--x), var(--y)) scale(2.4); }
}
@media (prefers-reduced-motion: reduce) {
.card-clapper *, .card-clapper__iris { animation: none !important; transition: none !important; }
}
`;
export function ClapperCard() {
const [phase, setPhase] = useState<Phase>('slate');
const [take, setTake] = useState(3);
const [num, setNum] = useState(3);
const timers = useRef<number[]>([]);
useEffect(() => () => timers.current.forEach((t) => window.clearTimeout(t)), []);
const later = (ms: number, fn: () => void) => timers.current.push(window.setTimeout(fn, ms));
const roll = () => {
if (phase !== 'slate') return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
setTake((t) => t + 1);
return;
}
timers.current = [];
setPhase('clap');
later(560, () => setPhase('drop'));
later(960, () => {
setNum(3);
setPhase('leader');
});
later(1760, () => setNum(2));
later(2560, () => setNum(1));
later(3360, () => setPhase('film'));
later(6600, () => setPhase('iris'));
later(7450, () => setPhase('end'));
later(8800, () => {
setTake((t) => t + 1);
setPhase('slate');
});
};
const rolling = phase === 'leader' || phase === 'film' || phase === 'iris';
return (
<>
<style>
{css}
</style>
<article className="card-clapper" data-phase={phase}>
<div className="card-clapper__screen" aria-hidden="true">
<div className="card-clapper__weave">
{phase === 'leader' && (
<div className="card-clapper__leader">
<svg viewBox="0 0 248 146">
<circle key={num} className="card-clapper__wipe" cx="124" cy="73" r="29" pathLength={1} />
<path className="card-clapper__cross" d="M0 73 H248 M124 0 V146" />
<circle className="card-clapper__circle" cx="124" cy="73" r="60" />
<circle className="card-clapper__circle" cx="124" cy="73" r="52" />
</svg>
<span className="card-clapper__num">{num}</span>
</div>
)}
{(phase === 'film' || phase === 'iris') && (
<div className="card-clapper__reel">
<span className="card-clapper__floor" />
<span className="card-clapper__ballshadow" />
<span className="card-clapper__ball" />
<span className="card-clapper__iris" />
</div>
)}
{rolling && <span className="card-clapper__scratch" />}
{phase === 'end' && <div className="card-clapper__end">The End</div>}
</div>
</div>
<button type="button" className="card-clapper__press" aria-label={`Play The Launch Film, take ${take}`} onClick={roll}>
<span className="card-clapper__stick card-clapper__stick--low" />
<span className="card-clapper__stick card-clapper__stick--top" />
<span className="card-clapper__hinge" />
{phase === 'clap' && (
<span className="card-clapper__dust" aria-hidden="true">
{DUST.map(([x, y], i) => (
<i key={i} style={{ '--x': `${x}px`, '--y': `${y}px`, '--s': `${3 + (i % 3)}px`, animationDelay: '0.12s' } as CSSProperties} />
))}
</span>
)}
<span className="card-clapper__board">
<span className="card-clapper__row">
<span className="card-clapper__cell">
<span className="card-clapper__label">Production</span>
<span className="card-clapper__value">The Launch Film</span>
</span>
</span>
<span className="card-clapper__row card-clapper__row--three">
<span className="card-clapper__cell">
<span className="card-clapper__label">Roll</span>
<span className="card-clapper__value card-clapper__value--big">A12</span>
</span>
<span className="card-clapper__cell">
<span className="card-clapper__label">Scene</span>
<span className="card-clapper__value card-clapper__value--big">7</span>
</span>
<span className="card-clapper__cell">
<span className="card-clapper__label">Take</span>
<span className="card-clapper__value card-clapper__value--big">
<span key={take} className="card-clapper__take">
{take}
</span>
</span>
</span>
</span>
<span className="card-clapper__row card-clapper__row--two">
<span className="card-clapper__cell">
<span className="card-clapper__label">Director</span>
<span className="card-clapper__value">M. Holt</span>
</span>
<span className="card-clapper__cell">
<span className="card-clapper__label">Runtime</span>
<span className="card-clapper__value">0:42</span>
</span>
</span>
</span>
</button>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardClapper(el) on the mounted element instead.
HTML
<article class="card-clapper" data-phase="slate">
<div class="card-clapper__screen" aria-hidden="true">
<div class="card-clapper__weave">
<div class="card-clapper__leader">
<svg viewBox="0 0 248 146">
<circle class="card-clapper__wipe" cx="124" cy="73" r="29" pathLength="1"></circle>
<path class="card-clapper__cross" d="M0 73 H248 M124 0 V146"></path>
<circle class="card-clapper__circle" cx="124" cy="73" r="60"></circle>
<circle class="card-clapper__circle" cx="124" cy="73" r="52"></circle>
</svg>
<span class="card-clapper__num">3</span>
</div>
<div class="card-clapper__reel">
<span class="card-clapper__floor"></span>
<span class="card-clapper__ballshadow"></span>
<span class="card-clapper__ball"></span>
<span class="card-clapper__iris"></span>
</div>
<span class="card-clapper__scratch"></span>
<div class="card-clapper__end">The End</div>
</div>
</div>
<button type="button" class="card-clapper__press" aria-label="Play The Launch Film, take 3">
<span class="card-clapper__stick card-clapper__stick--low"></span>
<span class="card-clapper__stick card-clapper__stick--top"></span>
<span class="card-clapper__hinge"></span>
<span class="card-clapper__dust" aria-hidden="true">
<i style="--x:-30px;--y:-18px;--s:3px;animation-delay:0.12s"></i>
<i style="--x:-16px;--y:-30px;--s:4px;animation-delay:0.12s"></i>
<i style="--x:4px;--y:-26px;--s:5px;animation-delay:0.12s"></i>
<i style="--x:22px;--y:-32px;--s:3px;animation-delay:0.12s"></i>
<i style="--x:36px;--y:-14px;--s:4px;animation-delay:0.12s"></i>
<i style="--x:-40px;--y:-6px;--s:5px;animation-delay:0.12s"></i>
<i style="--x:46px;--y:-4px;--s:3px;animation-delay:0.12s"></i>
</span>
<span class="card-clapper__board">
<span class="card-clapper__row">
<span class="card-clapper__cell">
<span class="card-clapper__label">Production</span>
<span class="card-clapper__value">The Launch Film</span>
</span>
</span>
<span class="card-clapper__row card-clapper__row--three">
<span class="card-clapper__cell">
<span class="card-clapper__label">Roll</span>
<span class="card-clapper__value card-clapper__value--big">A12</span>
</span>
<span class="card-clapper__cell">
<span class="card-clapper__label">Scene</span>
<span class="card-clapper__value card-clapper__value--big">7</span>
</span>
<span class="card-clapper__cell">
<span class="card-clapper__label">Take</span>
<span class="card-clapper__value card-clapper__value--big">
<span class="card-clapper__take">3</span>
</span>
</span>
</span>
<span class="card-clapper__row card-clapper__row--two">
<span class="card-clapper__cell">
<span class="card-clapper__label">Director</span>
<span class="card-clapper__value">M. Holt</span>
</span>
<span class="card-clapper__cell">
<span class="card-clapper__label">Runtime</span>
<span class="card-clapper__value">0:42</span>
</span>
</span>
</span>
</button>
</article>CSS
.card-clapper { flex: none; --slate: light-dark(#42454c, #232529); --chalk: light-dark(#fbfcfd, #f1f2f3); --stick: light-dark(#373a40, #121417); --film: light-dark(#6d7179, #7d8087); position: relative; width: 248px; height: 220px; color: var(--chalk); }
.card-clapper__screen { position: absolute; left: 0; right: 0; bottom: 8px; height: 146px; overflow: hidden; border-radius: 4px; background: var(--film); box-shadow: 0 8px 18px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.card-clapper__weave { position: absolute; inset: 0; }
.card-clapper[data-phase='leader'] .card-clapper__weave, .card-clapper[data-phase='film'] .card-clapper__weave, .card-clapper[data-phase='iris'] .card-clapper__weave { animation: card-clapper-weave 0.24s steps(3) infinite; }
@keyframes card-clapper-weave {
0% { transform: translate(0, 0); filter: brightness(1); }
33% { transform: translate(0.6px, -0.8px); filter: brightness(1.1); }
66% { transform: translate(-0.5px, 0.5px); filter: brightness(0.92); }
}
.card-clapper__leader { position: absolute; inset: 0; display: grid; place-items: center; background: var(--film); }
.card-clapper__leader svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.card-clapper__cross { stroke: var(--stick); stroke-width: 1; }
.card-clapper__circle { fill: none; stroke: var(--chalk); stroke-width: 2; }
.card-clapper__wipe { fill: none; stroke: var(--stick); stroke-opacity: 0.38; stroke-width: 58; stroke-dasharray: 1; transform: rotate(-90deg); transform-origin: 124px 73px; animation: card-clapper-wipe 0.8s linear infinite; }
@keyframes card-clapper-wipe {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
.card-clapper__num { position: relative; font: 700 62px/1 Georgia, 'Times New Roman', serif; color: var(--chalk); }
.card-clapper__reel { position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 46%, color-mix(in oklab, var(--chalk) 74%, var(--film)), var(--film) 92%); }
.card-clapper__floor { position: absolute; left: 0; right: 0; bottom: 0; height: 32px; background: linear-gradient(to bottom, var(--stick), color-mix(in oklab, var(--stick) 70%, var(--film))); }
.card-clapper__ball { position: absolute; left: 50%; bottom: 32px; width: 30px; height: 30px; margin-left: -15px; border-radius: 50%; background: var(--stick); transform-origin: 50% 100%; animation: card-clapper-bounce 0.92s linear infinite; }
@keyframes card-clapper-bounce {
0% { transform: translateY(0) scale(1.34, 0.66); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
12% { transform: translateY(-10px) scale(0.88, 1.16); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
50% { transform: translateY(-74px) scale(1, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
90% { transform: translateY(-10px) scale(0.88, 1.16); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
100% { transform: translateY(0) scale(1.34, 0.66); }
}
.card-clapper__ballshadow { position: absolute; left: 50%; bottom: 14px; width: 44px; height: 8px; margin-left: -22px; border-radius: 50%; background: var(--stick); opacity: 0.6; animation: card-clapper-pool 0.92s linear infinite; }
@keyframes card-clapper-pool {
0%, 100% { transform: scale(1); opacity: 1; animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
50% { transform: scale(0.42); opacity: 0.42; animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
}
.card-clapper__scratch { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--chalk); opacity: 0.5; animation: card-clapper-scratch 0.6s steps(5) infinite; }
@keyframes card-clapper-scratch {
0% { left: 18%; opacity: 0.5; }
25% { left: 73%; opacity: 0.2; }
50% { left: 41%; opacity: 0.55; }
75% { left: 88%; opacity: 0.12; }
100% { left: 26%; opacity: 0.4; }
}
.card-clapper__iris { position: absolute; inset: -60px; pointer-events: none; background: radial-gradient(circle at 50% 50%, transparent var(--card-clapper-open), var(--stick) calc(var(--card-clapper-open) + 1px)); }
.card-clapper[data-phase='iris'] .card-clapper__iris { animation: card-clapper-iris 0.85s ease-in forwards; }
@property --card-clapper-open { syntax: '<length>'; inherits: false; initial-value: 190px; }
@keyframes card-clapper-iris {
from { --card-clapper-open: 190px; }
to { --card-clapper-open: 0px; }
}
.card-clapper__end { position: absolute; inset: 0; display: grid; place-items: center; background: var(--stick); color: var(--chalk); font: italic 700 25px/1 Georgia, 'Times New Roman', serif; animation: card-clapper-end 0.3s ease both; }
@keyframes card-clapper-end {
from { opacity: 0; }
}
.card-clapper__press { position: absolute; left: 0; right: 0; bottom: 8px; height: 178px; padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; transform-origin: 50% 100%; transition: transform 0.42s cubic-bezier(0.55, 0, 0.9, 0.5), opacity 0.42s; }
.card-clapper__press:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 6px; }
.card-clapper[data-phase='drop'] .card-clapper__press, .card-clapper[data-phase='leader'] .card-clapper__press, .card-clapper[data-phase='film'] .card-clapper__press, .card-clapper[data-phase='iris'] .card-clapper__press, .card-clapper[data-phase='end'] .card-clapper__press { transform: translateY(250px) rotate(6deg); }
.card-clapper[data-phase='clap'] .card-clapper__press { animation: card-clapper-jolt 0.32s cubic-bezier(0.3, 1.6, 0.5, 1) 0.14s; }
@keyframes card-clapper-jolt {
0% { transform: translateY(3px) scale(1.012, 0.985); }
100% { transform: none; }
}
.card-clapper__stick { position: absolute; left: 0; right: 0; height: 20px; background: repeating-linear-gradient(-60deg, var(--stick) 0 15px, var(--chalk) 15px 30px); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)); }
.card-clapper__stick--top { top: 0; border-radius: 3px 3px 0 0; transform-origin: 9px 100%; transform: rotate(-8.5deg); transition: transform 0.34s cubic-bezier(0.3, 1.4, 0.5, 1); background: repeating-linear-gradient(60deg, var(--stick) 0 15px, var(--chalk) 15px 30px); }
.card-clapper__press:hover .card-clapper__stick--top { transform: rotate(-10.5deg); }
.card-clapper[data-phase='clap'] .card-clapper__stick--top, .card-clapper[data-phase='drop'] .card-clapper__stick--top { transform: rotate(0); transition: transform 0.13s cubic-bezier(0.7, 0, 1, 0.5); }
.card-clapper__stick--low { top: 20px; }
.card-clapper__hinge { position: absolute; left: 4px; top: 14px; width: 12px; height: 12px; border-radius: 50%; background: var(--film); box-shadow: 0 0 0 1.5px var(--stick); }
.card-clapper__board { position: absolute; left: 0; right: 0; top: 40px; bottom: 0; display: grid; grid-template-rows: 1fr 1.5fr 1fr; border-radius: 0 0 5px 5px; background: var(--slate); overflow: hidden; box-shadow: 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.card-clapper__row { display: grid; border-top: 1.5px solid color-mix(in oklab, var(--chalk) 66%, transparent); }
.card-clapper__row:first-child { border-top: 0; }
.card-clapper__row--three { grid-template-columns: 1.1fr 1fr 1fr; }
.card-clapper__row--two { grid-template-columns: 1fr 1fr; }
.card-clapper__cell { min-width: 0; padding: 3px 8px; border-left: 1.5px solid color-mix(in oklab, var(--chalk) 66%, transparent); }
.card-clapper__cell:first-child { border-left: 0; }
.card-clapper__label { display: block; font: 700 7.5px/1.1 system-ui, sans-serif; letter-spacing: 0.1em; text-transform: uppercase; }
.card-clapper__value { display: block; overflow: hidden; font-family: 'Chalkboard SE', 'Segoe Print', 'Bradley Hand', 'Comic Sans MS', cursive; font-size: 13.5px; line-height: 1.2; white-space: nowrap; }
.card-clapper__value--big { font-size: 25px; line-height: 1.08; }
.card-clapper__take { display: block; animation: card-clapper-take 0.3s cubic-bezier(0.3, 1.7, 0.5, 1); }
@keyframes card-clapper-take {
from { transform: scale(1.6); opacity: 0; }
}
.card-clapper__dust { position: absolute; left: 60%; top: 28px; width: 0; height: 0; pointer-events: none; }
.card-clapper__dust i { position: absolute; width: var(--s); height: var(--s); border-radius: 50%; background: var(--chalk); animation: card-clapper-dust 0.62s ease-out forwards; }
@keyframes card-clapper-dust {
from { opacity: 0.9; transform: none; }
to { opacity: 0; transform: translate(var(--x), var(--y)) scale(2.4); }
}
@media (prefers-reduced-motion: reduce) {
.card-clapper *, .card-clapper__iris { animation: none !important; transition: none !important; }
}
/* Everything that plays on the screen is in the markup and shown by phase. */
.card-clapper__leader, .card-clapper__reel, .card-clapper__scratch, .card-clapper__end, .card-clapper__dust { display: none; }
.card-clapper[data-phase='leader'] .card-clapper__leader { display: grid; }
.card-clapper[data-phase='film'] .card-clapper__reel, .card-clapper[data-phase='iris'] .card-clapper__reel { display: block; }
.card-clapper[data-phase='leader'] .card-clapper__scratch, .card-clapper[data-phase='film'] .card-clapper__scratch, .card-clapper[data-phase='iris'] .card-clapper__scratch { display: block; }
.card-clapper[data-phase='end'] .card-clapper__end { display: grid; }
.card-clapper[data-phase='clap'] .card-clapper__dust { display: block; }JavaScript
function initCardClapper(root) {
var press = root.querySelector('.card-clapper__press');
var takeEl = root.querySelector('.card-clapper__take');
var num = root.querySelector('.card-clapper__num');
var phase = 'slate';
var take = 3;
var timers = [];
function set(next) {
phase = next;
root.setAttribute('data-phase', next);
}
function setTake(n) {
take = n;
takeEl.textContent = n;
press.setAttribute('aria-label', 'Play The Launch Film, take ' + n);
// Replay the chalk-up on the new number.
takeEl.style.animation = 'none';
void takeEl.offsetWidth;
takeEl.style.animation = '';
}
// The whole reel is one timeline: clap, drop, countdown, film, iris, The End.
function onClick() {
if (phase !== 'slate') return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
setTake(take + 1);
return;
}
set('clap');
timers = [
[560, function () { set('drop'); }],
[960, function () { num.textContent = '3'; set('leader'); }],
[1760, function () { num.textContent = '2'; }],
[2560, function () { num.textContent = '1'; }],
[3360, function () { set('film'); }],
[6600, function () { set('iris'); }],
[7450, function () { set('end'); }],
[8800, function () { setTake(take + 1); set('slate'); }],
].map(function (step) {
return setTimeout(step[1], step[0]);
});
}
press.addEventListener('click', onClick);
return function () {
timers.forEach(clearTimeout);
press.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.card-clapper').forEach(initCardClapper);Goal: Create a "Clapper" card.
Visual behavior: A video card as a film slate. Press and the sticks snap shut in a puff of chalk, the slate drops away, the old film countdown runs, and a silent reel plays until the iris closes on The End.
Technique:
- One timeline of timers steps a data-phase through clap, drop, leader, film, iris and end. The take number goes up when it comes back to the slate.
- The countdown wipe is a circle with a stroke as wide as its radius, drawn in by its dash offset. The iris is a radial gradient whose radius is a registered @property, which is what lets it animate. Stepped jitter, brightness flicker and a jumping scratch give the reel its age.
Accessibility: The slate is a real button whose aria-label names the film and take; the screen is aria-hidden. Under prefers-reduced-motion nothing plays and the take simply goes up.
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 "Clapper" card.
Visual behavior: A video card as a film slate. Press and the sticks snap shut in a puff of chalk, the slate drops away, the old film countdown runs, and a silent reel plays until the iris closes on The End.
Technique:
- One timeline of timers steps a data-phase through clap, drop, leader, film, iris and end. The take number goes up when it comes back to the slate.
- The countdown wipe is a circle with a stroke as wide as its radius, drawn in by its dash offset. The iris is a radial gradient whose radius is a registered @property, which is what lets it animate. Stepped jitter, brightness flicker and a jumping scratch give the reel its age.
Accessibility: The slate is a real button whose aria-label names the film and take; the screen is aria-hidden. Under prefers-reduced-motion nothing plays and the take simply goes up.
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 video card as a film slate. Press and the sticks snap shut in a puff of chalk, the slate drops away, the old film countdown runs, and a silent reel plays until the iris closes on The End.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- card, click, state change, keyframe
- Techniques
- keyframes, state machine, css property, stroke dashoffset, radial gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The slate is a real button whose aria-label names the film and take; the screen is aria-hidden. Under prefers-reduced-motion nothing plays and the take simply goes up.
More cards
See allPick your hand
Your profile6 of 9
Next: Add a photo
API keysFour on the ring. Pick one.
Ana Kerr, Art director, 020 7946 0112