Specimen
import { useEffect, useRef, useState } from 'react';
const css = `
.card-specimen { flex: none; --mat: light-dark(#fbfcfd, #e5e6e9); --print: light-dark(#373a40, #121417); --wing: light-dark(#cbced3, #7d8087); --wash: light-dark(#ebedf0, #c6c8cd); --flap: 0deg; position: relative; box-sizing: border-box; width: 196px; height: 226px; padding: 7px; border-radius: 3px; background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#cbced3, #393c41)); box-shadow: 0 10px 20px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); color: var(--print); }
.card-specimen__case { position: relative; display: block; height: 100%; overflow: visible; border-radius: 1px; background: var(--mat); box-shadow: inset 0 2px 7px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)); }
.card-specimen__bug { position: absolute; left: 50%; top: 18px; width: 0; height: 0; z-index: 3; }
.card-specimen__bug[data-flying='out'] { animation: card-specimen-out 2s cubic-bezier(0.4, 0, 0.7, 1) forwards; }
.card-specimen__bug[data-flying='in'] { animation: card-specimen-in 1.9s cubic-bezier(0.2, 0.5, 0.4, 1) forwards; }
@keyframes card-specimen-out {
0% { transform: none; }
16% { transform: translate(6px, -14px) rotate(8deg) scale(1.02); }
34% { transform: translate(-34px, -34px) rotate(-16deg) scale(0.92); }
52% { transform: translate(26px, -12px) rotate(20deg) scale(0.84); }
72% { transform: translate(60px, -70px) rotate(34deg) scale(0.66); }
100% { transform: translate(180px, -160px) rotate(52deg) scale(0.42); opacity: 0.7; }
}
@keyframes card-specimen-in {
0% { transform: translate(-170px, -130px) rotate(-40deg) scale(0.42); }
44% { transform: translate(-44px, -56px) rotate(-22deg) scale(0.74); }
70% { transform: translate(24px, -30px) rotate(14deg) scale(0.92); }
88% { transform: translate(-5px, -8px) rotate(-5deg) scale(1.02); }
100% { transform: none; }
}
.card-specimen__wing { position: absolute; top: 0; width: 80px; height: 112px; overflow: visible; filter: drop-shadow(2px 4px 2px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28))); transition: transform 0.4s cubic-bezier(0.3, 1.3, 0.5, 1); }
.card-specimen__wing--r { left: 1px; transform-origin: 0 50%; transform: perspective(300px) rotateY(var(--flap)); }
.card-specimen__wing--l { right: 1px; transform-origin: 100% 50%; transform: perspective(300px) rotateY(calc(var(--flap) * -1)) scaleX(1); }
.card-specimen__wing--l svg { transform: scaleX(-1); }
.card-specimen__wing svg { display: block; width: 100%; height: 100%; overflow: visible; }
.card-specimen__bug[data-flying] .card-specimen__wing { transition: none; animation: card-specimen-beat 0.15s ease-in-out infinite alternate; }
.card-specimen__bug[data-flying] .card-specimen__wing--r { --way: 1; }
.card-specimen__bug[data-flying] .card-specimen__wing--l { --way: -1; }
@keyframes card-specimen-beat {
from { transform: perspective(300px) rotateY(calc(var(--way) * 8deg)); }
to { transform: perspective(300px) rotateY(calc(var(--way) * 70deg)); }
}
.card-specimen__fill { fill: var(--wing); }
.card-specimen__wash { fill: var(--wash); }
.card-specimen__band { fill: var(--print); }
.card-specimen__spot { fill: var(--mat); }
.card-specimen__vein { fill: none; stroke: var(--print); stroke-width: 0.8; opacity: 0.72; stroke-linecap: round; }
.card-specimen__body { position: absolute; left: -4px; top: 18px; width: 8px; height: 74px; overflow: visible; }
.card-specimen__ink { fill: var(--print); }
.card-specimen__antenna { fill: none; stroke: var(--print); stroke-width: 0.8; stroke-linecap: round; }
.card-specimen__pin { position: absolute; left: 50%; top: 42px; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, var(--mat), light-dark(#6d7179, #7d8087) 60%); box-shadow: 1px 3px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); z-index: 4; transition: transform 0.4s cubic-bezier(0.5, 0, 0.8, 0.5), opacity 0.4s; }
.card-specimen[data-free='true'] .card-specimen__pin { transform: translate(44px, 86px) rotate(70deg); }
.card-specimen__ghost { position: absolute; left: 50%; top: 42px; width: 3px; height: 3px; margin: 3px 0 0 -1.5px; border-radius: 50%; background: var(--print); opacity: 0; transition: opacity 0.3s 0.3s; }
.card-specimen[data-free='true'] .card-specimen__ghost { opacity: 0.7; }
.card-specimen__dust { position: absolute; left: 50%; top: 18px; width: 148px; height: 112px; margin-left: -74px; background: radial-gradient(ellipse 40% 34% at 28% 34%, color-mix(in oklab, var(--print) 13%, transparent), transparent), radial-gradient(ellipse 40% 34% at 72% 34%, color-mix(in oklab, var(--print) 13%, transparent), transparent), radial-gradient(ellipse 26% 28% at 36% 74%, color-mix(in oklab, var(--print) 10%, transparent), transparent), radial-gradient(ellipse 26% 28% at 64% 74%, color-mix(in oklab, var(--print) 10%, transparent), transparent); opacity: 0; transition: opacity 0.8s 0.3s; }
.card-specimen[data-free='true'] .card-specimen__dust { opacity: 1; }
.card-specimen__label { position: absolute; left: 14px; right: 14px; bottom: 9px; padding: 6px 8px 5px; border: 1px solid color-mix(in oklab, var(--print) 50%, transparent); background: var(--mat); text-align: center; }
.card-specimen__latin { display: block; font: italic 700 13px/1.1 Georgia, 'Times New Roman', serif; }
.card-specimen[data-free='true'] .card-specimen__latin { text-decoration: line-through; }
.card-specimen__plate { display: block; margin-top: 2px; font: 9px/1.2 Georgia, 'Times New Roman', serif; letter-spacing: 0.02em; }
.card-specimen__act { margin-top: 3px; padding: 1px 2px; border: 0; background: none; color: inherit; font: italic 700 10.5px/1.3 Georgia, 'Times New Roman', serif; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.card-specimen__act:focus-visible { outline: 2px solid var(--print); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) {
.card-specimen__bug[data-flying] { animation-duration: 0.01s; }
.card-specimen__bug[data-flying] .card-specimen__wing { animation: none; }
.card-specimen__wing, .card-specimen__pin { transition: none; }
}
`;
function Wing() {
return (
<svg viewBox="0 0 80 112" aria-hidden="true">
{/* Hindwing first, so the forewing overlaps it as on the real thing. */}
<path className="card-specimen__fill" d="M0 50 C14 50 40 50 52 62 C60 74 54 92 44 98 C42 106 34 110 30 102 C20 96 6 78 0 60 Z" />
<path className="card-specimen__band" d="M50 60 C60 74 54 92 44 98 C42 106 34 110 30 102 C28 100 26 98 24 95 C34 96 44 84 42 66 Z" />
<circle className="card-specimen__spot" cx="48" cy="72" r="2.3" />
<circle className="card-specimen__spot" cx="46.5" cy="82" r="2.3" />
<circle className="card-specimen__spot" cx="40.5" cy="91" r="2.2" />
<circle className="card-specimen__spot" cx="33" cy="99" r="2.1" />
<circle className="card-specimen__band" cx="20" cy="74" r="6.2" />
<circle className="card-specimen__wash" cx="20" cy="74" r="3.8" />
<circle className="card-specimen__band" cx="20" cy="74" r="1.7" />
<path className="card-specimen__vein" d="M1 54 C14 58 30 58 42 64 M1 56 C10 66 20 78 24 92 M1 58 C4 68 10 80 16 88" />
<path className="card-specimen__fill" d="M0 32 C8 8 44 -3 78 7 C80 22 68 44 44 51 C28 55 10 50 0 46 Z" />
<path className="card-specimen__band" d="M78 7 C80 22 68 44 44 51 C42 52 40 52 38 53 C54 40 64 26 62 6 C68 5 73 5.6 78 7 Z" />
<path className="card-specimen__wash" d="M6 28 C18 14 34 8 48 6 C38 16 26 26 8 36 Z" />
<circle className="card-specimen__spot" cx="70" cy="13" r="2.2" />
<circle className="card-specimen__spot" cx="68" cy="24" r="2.2" />
<circle className="card-specimen__spot" cx="60" cy="34" r="2.1" />
<circle className="card-specimen__spot" cx="50" cy="43" r="2" />
<path className="card-specimen__vein" d="M1 34 C16 20 36 12 60 8 M1 38 C18 30 40 24 62 20 M1 42 C20 40 40 38 56 32 M1 45 C14 46 30 46 44 44" />
</svg>
);
}
export function SpecimenCard() {
const root = useRef<HTMLElement>(null);
const [state, setState] = useState<'pinned' | 'leaving' | 'free' | 'landing'>('pinned');
const stateRef = useRef(state);
stateRef.current = state;
const timer = useRef(0);
// Wings stir as you come close: the nearer the cursor, the further they lift.
useEffect(() => {
const el = root.current;
if (!el || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const stir = (e: PointerEvent) => {
if (stateRef.current !== 'pinned') return;
const b = el.getBoundingClientRect();
const d = Math.hypot(e.clientX - (b.left + b.width / 2), e.clientY - (b.top + 70));
el.style.setProperty('--flap', `${(Math.max(0, 1 - d / 150) * 42).toFixed(1)}deg`);
};
const rest = () => el.style.setProperty('--flap', '0deg');
el.addEventListener('pointermove', stir);
el.addEventListener('pointerleave', rest);
return () => {
window.clearTimeout(timer.current);
el.removeEventListener('pointermove', stir);
el.removeEventListener('pointerleave', rest);
};
}, []);
const act = () => {
window.clearTimeout(timer.current);
root.current?.style.setProperty('--flap', '0deg');
if (state === 'pinned') {
setState('leaving');
timer.current = window.setTimeout(() => setState('free'), 2000);
} else if (state === 'free') {
setState('landing');
timer.current = window.setTimeout(() => setState('pinned'), 1900);
}
};
const gone = state === 'free';
return (
<>
<style>
{css}
</style>
<article ref={root} className="card-specimen" data-free={state === 'leaving' || state === 'free'}>
<div className="card-specimen__case">
<span className="card-specimen__dust" aria-hidden="true" />
<span className="card-specimen__ghost" aria-hidden="true" />
{!gone && (
<div className="card-specimen__bug" data-flying={state === 'leaving' ? 'out' : state === 'landing' ? 'in' : undefined} aria-hidden="true">
<span className="card-specimen__wing card-specimen__wing--r">
<Wing />
</span>
<span className="card-specimen__wing card-specimen__wing--l">
<Wing />
</span>
<svg className="card-specimen__body" viewBox="0 0 8 74">
<path className="card-specimen__antenna" d="M3 8 C0 2 -6 -6 -12 -11 M5 8 C8 2 14 -6 20 -11" />
<circle className="card-specimen__ink" cx="-12" cy="-11" r="1.2" />
<circle className="card-specimen__ink" cx="20" cy="-11" r="1.2" />
<circle className="card-specimen__ink" cx="4" cy="9" r="3.3" />
<path className="card-specimen__ink" d="M4 11 C7.6 16 7.6 28 6.4 40 C5.6 52 5 64 4 72 C3 64 2.4 52 1.6 40 C0.4 28 0.4 16 4 11 Z" />
</svg>
</div>
)}
<span className="card-specimen__pin" aria-hidden="true" />
<div className="card-specimen__label">
<span className="card-specimen__latin">Papilio machaon</span>
<span className="card-specimen__plate">Swallowtail, plate IV</span>
<button type="button" className="card-specimen__act" onClick={act} disabled={state === 'leaving' || state === 'landing'}>
{state === 'pinned' || state === 'leaving' ? 'Release it' : 'Call it back'}
</button>
</div>
</div>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardSpecimen(el) on the mounted element instead.
<article class="card-specimen" data-free="false">
<div class="card-specimen__case">
<span class="card-specimen__dust" aria-hidden="true"></span>
<span class="card-specimen__ghost" aria-hidden="true"></span>
<div class="card-specimen__bug" aria-hidden="true">
<span class="card-specimen__wing card-specimen__wing--r">
<svg viewBox="0 0 80 112" aria-hidden="true">
<path class="card-specimen__fill" d="M0 50 C14 50 40 50 52 62 C60 74 54 92 44 98 C42 106 34 110 30 102 C20 96 6 78 0 60 Z"></path>
<path class="card-specimen__band" d="M50 60 C60 74 54 92 44 98 C42 106 34 110 30 102 C28 100 26 98 24 95 C34 96 44 84 42 66 Z"></path>
<circle class="card-specimen__spot" cx="48" cy="72" r="2.3"></circle>
<circle class="card-specimen__spot" cx="46.5" cy="82" r="2.3"></circle>
<circle class="card-specimen__spot" cx="40.5" cy="91" r="2.2"></circle>
<circle class="card-specimen__spot" cx="33" cy="99" r="2.1"></circle>
<circle class="card-specimen__band" cx="20" cy="74" r="6.2"></circle>
<circle class="card-specimen__wash" cx="20" cy="74" r="3.8"></circle>
<circle class="card-specimen__band" cx="20" cy="74" r="1.7"></circle>
<path class="card-specimen__vein" d="M1 54 C14 58 30 58 42 64 M1 56 C10 66 20 78 24 92 M1 58 C4 68 10 80 16 88"></path>
<path class="card-specimen__fill" d="M0 32 C8 8 44 -3 78 7 C80 22 68 44 44 51 C28 55 10 50 0 46 Z"></path>
<path class="card-specimen__band" d="M78 7 C80 22 68 44 44 51 C42 52 40 52 38 53 C54 40 64 26 62 6 C68 5 73 5.6 78 7 Z"></path>
<path class="card-specimen__wash" d="M6 28 C18 14 34 8 48 6 C38 16 26 26 8 36 Z"></path>
<circle class="card-specimen__spot" cx="70" cy="13" r="2.2"></circle>
<circle class="card-specimen__spot" cx="68" cy="24" r="2.2"></circle>
<circle class="card-specimen__spot" cx="60" cy="34" r="2.1"></circle>
<circle class="card-specimen__spot" cx="50" cy="43" r="2"></circle>
<path class="card-specimen__vein" d="M1 34 C16 20 36 12 60 8 M1 38 C18 30 40 24 62 20 M1 42 C20 40 40 38 56 32 M1 45 C14 46 30 46 44 44"></path>
</svg>
</span>
<span class="card-specimen__wing card-specimen__wing--l">
<svg viewBox="0 0 80 112" aria-hidden="true">
<path class="card-specimen__fill" d="M0 50 C14 50 40 50 52 62 C60 74 54 92 44 98 C42 106 34 110 30 102 C20 96 6 78 0 60 Z"></path>
<path class="card-specimen__band" d="M50 60 C60 74 54 92 44 98 C42 106 34 110 30 102 C28 100 26 98 24 95 C34 96 44 84 42 66 Z"></path>
<circle class="card-specimen__spot" cx="48" cy="72" r="2.3"></circle>
<circle class="card-specimen__spot" cx="46.5" cy="82" r="2.3"></circle>
<circle class="card-specimen__spot" cx="40.5" cy="91" r="2.2"></circle>
<circle class="card-specimen__spot" cx="33" cy="99" r="2.1"></circle>
<circle class="card-specimen__band" cx="20" cy="74" r="6.2"></circle>
<circle class="card-specimen__wash" cx="20" cy="74" r="3.8"></circle>
<circle class="card-specimen__band" cx="20" cy="74" r="1.7"></circle>
<path class="card-specimen__vein" d="M1 54 C14 58 30 58 42 64 M1 56 C10 66 20 78 24 92 M1 58 C4 68 10 80 16 88"></path>
<path class="card-specimen__fill" d="M0 32 C8 8 44 -3 78 7 C80 22 68 44 44 51 C28 55 10 50 0 46 Z"></path>
<path class="card-specimen__band" d="M78 7 C80 22 68 44 44 51 C42 52 40 52 38 53 C54 40 64 26 62 6 C68 5 73 5.6 78 7 Z"></path>
<path class="card-specimen__wash" d="M6 28 C18 14 34 8 48 6 C38 16 26 26 8 36 Z"></path>
<circle class="card-specimen__spot" cx="70" cy="13" r="2.2"></circle>
<circle class="card-specimen__spot" cx="68" cy="24" r="2.2"></circle>
<circle class="card-specimen__spot" cx="60" cy="34" r="2.1"></circle>
<circle class="card-specimen__spot" cx="50" cy="43" r="2"></circle>
<path class="card-specimen__vein" d="M1 34 C16 20 36 12 60 8 M1 38 C18 30 40 24 62 20 M1 42 C20 40 40 38 56 32 M1 45 C14 46 30 46 44 44"></path>
</svg>
</span>
<svg class="card-specimen__body" viewBox="0 0 8 74">
<path class="card-specimen__antenna" d="M3 8 C0 2 -6 -6 -12 -11 M5 8 C8 2 14 -6 20 -11"></path>
<circle class="card-specimen__ink" cx="-12" cy="-11" r="1.2"></circle>
<circle class="card-specimen__ink" cx="20" cy="-11" r="1.2"></circle>
<circle class="card-specimen__ink" cx="4" cy="9" r="3.3"></circle>
<path class="card-specimen__ink" d="M4 11 C7.6 16 7.6 28 6.4 40 C5.6 52 5 64 4 72 C3 64 2.4 52 1.6 40 C0.4 28 0.4 16 4 11 Z"></path>
</svg>
</div>
<span class="card-specimen__pin" aria-hidden="true"></span>
<div class="card-specimen__label">
<span class="card-specimen__latin">Papilio machaon</span>
<span class="card-specimen__plate">Swallowtail, plate IV</span>
<button type="button" class="card-specimen__act">Release it</button>
</div>
</div>
</article>.card-specimen { flex: none; --mat: light-dark(#fbfcfd, #e5e6e9); --print: light-dark(#373a40, #121417); --wing: light-dark(#cbced3, #7d8087); --wash: light-dark(#ebedf0, #c6c8cd); --flap: 0deg; position: relative; box-sizing: border-box; width: 196px; height: 226px; padding: 7px; border-radius: 3px; background: linear-gradient(135deg, light-dark(#5f626a, #a7abb2), light-dark(#cbced3, #393c41)); box-shadow: 0 10px 20px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); color: var(--print); }
.card-specimen__case { position: relative; display: block; height: 100%; overflow: visible; border-radius: 1px; background: var(--mat); box-shadow: inset 0 2px 7px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28)); }
.card-specimen__bug { position: absolute; left: 50%; top: 18px; width: 0; height: 0; z-index: 3; }
.card-specimen__bug[data-flying='out'] { animation: card-specimen-out 2s cubic-bezier(0.4, 0, 0.7, 1) forwards; }
.card-specimen__bug[data-flying='in'] { animation: card-specimen-in 1.9s cubic-bezier(0.2, 0.5, 0.4, 1) forwards; }
@keyframes card-specimen-out {
0% { transform: none; }
16% { transform: translate(6px, -14px) rotate(8deg) scale(1.02); }
34% { transform: translate(-34px, -34px) rotate(-16deg) scale(0.92); }
52% { transform: translate(26px, -12px) rotate(20deg) scale(0.84); }
72% { transform: translate(60px, -70px) rotate(34deg) scale(0.66); }
100% { transform: translate(180px, -160px) rotate(52deg) scale(0.42); opacity: 0.7; }
}
@keyframes card-specimen-in {
0% { transform: translate(-170px, -130px) rotate(-40deg) scale(0.42); }
44% { transform: translate(-44px, -56px) rotate(-22deg) scale(0.74); }
70% { transform: translate(24px, -30px) rotate(14deg) scale(0.92); }
88% { transform: translate(-5px, -8px) rotate(-5deg) scale(1.02); }
100% { transform: none; }
}
.card-specimen__wing { position: absolute; top: 0; width: 80px; height: 112px; overflow: visible; filter: drop-shadow(2px 4px 2px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.28))); transition: transform 0.4s cubic-bezier(0.3, 1.3, 0.5, 1); }
.card-specimen__wing--r { left: 1px; transform-origin: 0 50%; transform: perspective(300px) rotateY(var(--flap)); }
.card-specimen__wing--l { right: 1px; transform-origin: 100% 50%; transform: perspective(300px) rotateY(calc(var(--flap) * -1)) scaleX(1); }
.card-specimen__wing--l svg { transform: scaleX(-1); }
.card-specimen__wing svg { display: block; width: 100%; height: 100%; overflow: visible; }
.card-specimen__bug[data-flying] .card-specimen__wing { transition: none; animation: card-specimen-beat 0.15s ease-in-out infinite alternate; }
.card-specimen__bug[data-flying] .card-specimen__wing--r { --way: 1; }
.card-specimen__bug[data-flying] .card-specimen__wing--l { --way: -1; }
@keyframes card-specimen-beat {
from { transform: perspective(300px) rotateY(calc(var(--way) * 8deg)); }
to { transform: perspective(300px) rotateY(calc(var(--way) * 70deg)); }
}
.card-specimen__fill { fill: var(--wing); }
.card-specimen__wash { fill: var(--wash); }
.card-specimen__band { fill: var(--print); }
.card-specimen__spot { fill: var(--mat); }
.card-specimen__vein { fill: none; stroke: var(--print); stroke-width: 0.8; opacity: 0.72; stroke-linecap: round; }
.card-specimen__body { position: absolute; left: -4px; top: 18px; width: 8px; height: 74px; overflow: visible; }
.card-specimen__ink { fill: var(--print); }
.card-specimen__antenna { fill: none; stroke: var(--print); stroke-width: 0.8; stroke-linecap: round; }
.card-specimen__pin { position: absolute; left: 50%; top: 42px; width: 8px; height: 8px; margin-left: -4px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, var(--mat), light-dark(#6d7179, #7d8087) 60%); box-shadow: 1px 3px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); z-index: 4; transition: transform 0.4s cubic-bezier(0.5, 0, 0.8, 0.5), opacity 0.4s; }
.card-specimen[data-free='true'] .card-specimen__pin { transform: translate(44px, 86px) rotate(70deg); }
.card-specimen__ghost { position: absolute; left: 50%; top: 42px; width: 3px; height: 3px; margin: 3px 0 0 -1.5px; border-radius: 50%; background: var(--print); opacity: 0; transition: opacity 0.3s 0.3s; }
.card-specimen[data-free='true'] .card-specimen__ghost { opacity: 0.7; }
.card-specimen__dust { position: absolute; left: 50%; top: 18px; width: 148px; height: 112px; margin-left: -74px; background: radial-gradient(ellipse 40% 34% at 28% 34%, color-mix(in oklab, var(--print) 13%, transparent), transparent), radial-gradient(ellipse 40% 34% at 72% 34%, color-mix(in oklab, var(--print) 13%, transparent), transparent), radial-gradient(ellipse 26% 28% at 36% 74%, color-mix(in oklab, var(--print) 10%, transparent), transparent), radial-gradient(ellipse 26% 28% at 64% 74%, color-mix(in oklab, var(--print) 10%, transparent), transparent); opacity: 0; transition: opacity 0.8s 0.3s; }
.card-specimen[data-free='true'] .card-specimen__dust { opacity: 1; }
.card-specimen__label { position: absolute; left: 14px; right: 14px; bottom: 9px; padding: 6px 8px 5px; border: 1px solid color-mix(in oklab, var(--print) 50%, transparent); background: var(--mat); text-align: center; }
.card-specimen__latin { display: block; font: italic 700 13px/1.1 Georgia, 'Times New Roman', serif; }
.card-specimen[data-free='true'] .card-specimen__latin { text-decoration: line-through; }
.card-specimen__plate { display: block; margin-top: 2px; font: 9px/1.2 Georgia, 'Times New Roman', serif; letter-spacing: 0.02em; }
.card-specimen__act { margin-top: 3px; padding: 1px 2px; border: 0; background: none; color: inherit; font: italic 700 10.5px/1.3 Georgia, 'Times New Roman', serif; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.card-specimen__act:focus-visible { outline: 2px solid var(--print); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) {
.card-specimen__bug[data-flying] { animation-duration: 0.01s; }
.card-specimen__bug[data-flying] .card-specimen__wing { animation: none; }
.card-specimen__wing, .card-specimen__pin { transition: none; }
}function initCardSpecimen(root) {
var bug = root.querySelector('.card-specimen__bug');
var act = root.querySelector('.card-specimen__act');
var state = 'pinned';
var timer = 0;
function set(next) {
state = next;
root.setAttribute('data-free', String(next === 'leaving' || next === 'free'));
bug.hidden = next === 'free';
if (next === 'leaving') bug.setAttribute('data-flying', 'out');
else if (next === 'landing') bug.setAttribute('data-flying', 'in');
else bug.removeAttribute('data-flying');
act.disabled = next === 'leaving' || next === 'landing';
act.textContent = next === 'pinned' || next === 'leaving' ? 'Release it' : 'Call it back';
}
// Wings stir as you come close: the nearer the cursor, the further they lift.
function onStir(e) {
if (state !== 'pinned' || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var b = root.getBoundingClientRect();
var d = Math.hypot(e.clientX - (b.left + b.width / 2), e.clientY - (b.top + 70));
root.style.setProperty('--flap', (Math.max(0, 1 - d / 150) * 42).toFixed(1) + 'deg');
}
function onRest() {
root.style.setProperty('--flap', '0deg');
}
function onAct() {
clearTimeout(timer);
onRest();
if (state === 'pinned') {
set('leaving');
timer = setTimeout(function () {
set('free');
}, 2000);
} else if (state === 'free') {
set('landing');
timer = setTimeout(function () {
set('pinned');
}, 1900);
}
}
root.addEventListener('pointermove', onStir);
root.addEventListener('pointerleave', onRest);
act.addEventListener('click', onAct);
return function () {
clearTimeout(timer);
root.removeEventListener('pointermove', onStir);
root.removeEventListener('pointerleave', onRest);
act.removeEventListener('click', onAct);
};
}
document.querySelectorAll('.card-specimen').forEach(initCardSpecimen);Goal: Create a "Specimen" card.
Visual behavior: A collection item as a butterfly pinned in a museum case with its Latin label. Its wings stir as you come near; release it and it flaps off the page, leaving just the pin.
Technique:
- Each wing is the same drawing, one mirrored, turned about the body with its own perspective, so the shadow can be a plain drop-shadow on each wing. Pointer distance sets a --flap variable, so they lift as you come close.
- Releasing plays a looping wing beat under a flight path of keyframes, the pin drops and a faint dust outline stays on the paper. Calling it back flies a second path in.
Accessibility: An article with a real Release button whose label follows the state; the butterfly is aria-hidden. Under prefers-reduced-motion the wings do not beat or stir and the butterfly moves 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 "Specimen" card.
Visual behavior: A collection item as a butterfly pinned in a museum case with its Latin label. Its wings stir as you come near; release it and it flaps off the page, leaving just the pin.
Technique:
- Each wing is the same drawing, one mirrored, turned about the body with its own perspective, so the shadow can be a plain drop-shadow on each wing. Pointer distance sets a --flap variable, so they lift as you come close.
- Releasing plays a looping wing beat under a flight path of keyframes, the pin drops and a faint dust outline stays on the paper. Calling it back flies a second path in.
Accessibility: An article with a real Release button whose label follows the state; the butterfly is aria-hidden. Under prefers-reduced-motion the wings do not beat or stir and the butterfly moves 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 collection item as a butterfly pinned in a museum case with its Latin label. Its wings stir as you come near; release it and it flaps off the page, leaving just the pin.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- card, click, mouse tracking, 3d
- Techniques
- keyframes, perspective, event position, state machine, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
An article with a real Release button whose label follows the state; the butterfly is aria-hidden. Under prefers-reduced-motion the wings do not beat or stir and the butterfly moves at once.