Your profile6 of 9
Next: Add a photo
Pick your hand
import { useEffect, useRef, useState, type CSSProperties } from 'react';
type Suit = 'club' | 'diamond' | 'heart' | 'spade';
const PIPS: Record<Suit, string> = {
spade: 'M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z',
heart: 'M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z',
diamond: 'M12 2 L20 12 L12 22 L4 12 Z',
club: 'M12 2.5 A5 5 0 0 1 16.6 9.5 A5 5 0 1 1 13.4 17.6 C13.6 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.4 19.4 10.6 17.6 A5 5 0 1 1 7.4 9.5 A5 5 0 0 1 12 2.5 Z',
};
// Where the pips sit on a real card, in percent of the card.
const LAYOUT: Record<string, [number, number][]> = {
'2': [[50, 24], [50, 76]],
'7': [[32, 22], [68, 22], [50, 36], [32, 50], [68, 50], [32, 78], [68, 78]],
'10': [[32, 19], [68, 19], [50, 30], [32, 41], [68, 41], [32, 59], [68, 59], [50, 70], [32, 81], [68, 81]],
};
const CARDS: { rank: string; suit: Suit; word: string; plan: string; price: string; note: string }[] = [
{ rank: '2', suit: 'club', word: 'Two of clubs', plan: 'Free', price: '$0', note: 'One project' },
{ rank: '7', suit: 'diamond', word: 'Seven of diamonds', plan: 'Solo', price: '$8/mo', note: 'Ten projects' },
{ rank: '10', suit: 'heart', word: 'Ten of hearts', plan: 'Team', price: '$24/mo', note: 'Five seats' },
{ rank: 'K', suit: 'spade', word: 'King of spades', plan: 'Studio', price: '$64/mo', note: 'Unlimited seats' },
{ rank: 'A', suit: 'spade', word: 'Ace of spades', plan: 'Forever', price: '$480 once', note: 'Yours for life' },
];
const css = `
.card-hand { flex: none; --face: light-dark(#fbfcfd, #f1f2f3); --black: light-dark(#373a40, #121417); --red: color-mix(in oklab, var(--black) 72%, var(--face)); position: relative; width: 270px; height: 226px; }
.card-hand__say { position: absolute; left: 0; right: 0; top: 2px; margin: 0; color: light-dark(#50535b, #c6c8cd); font-size: 13px; text-align: center; transition: opacity 0.2s; }
.card-hand__say b { color: light-dark(#373a40, #f1f2f3); font-weight: 700; }
.card-hand[data-played='true'] .card-hand__say { opacity: 0; }
.card-hand__card { --a: 0deg; position: absolute; left: 50%; bottom: 34px; width: 84px; height: 118px; margin-left: -42px; padding: 0; border: 0; border-radius: 8px; background: var(--face); color: var(--ink); font: inherit; cursor: pointer; box-shadow: -2px 2px 7px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.32)), 0 0 0 1px light-dark(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.1)); transform-origin: 50% 100%; transform: translate(var(--x), var(--y)) rotate(var(--a)); transition: transform 0.42s cubic-bezier(0.3, 1.25, 0.5, 1); }
.card-hand__card:focus-visible { outline: 2px solid light-dark(#373a40, #f1f2f3); outline-offset: 2px; }
.card-hand__corner { position: absolute; display: flex; flex-direction: column; align-items: center; width: 16px; font: 700 15px/1 Georgia, 'Times New Roman', serif; letter-spacing: -0.05em; }
.card-hand__corner svg { width: 11px; height: 11px; margin-top: 1px; fill: currentColor; }
.card-hand__corner--a { left: 5px; top: 6px; }
.card-hand__corner--b { right: 5px; bottom: 6px; transform: rotate(180deg); }
.card-hand__pip { position: absolute; width: 15px; height: 15px; margin: -7.5px; fill: currentColor; }
.card-hand__pip--low { transform: rotate(180deg); }
.card-hand__big { position: absolute; left: 50%; top: 50%; fill: currentColor; }
.card-hand__big--ace { width: 44px; height: 44px; margin: -22px; }
.card-hand__big--king { display: block; font: 700 54px/1 Georgia, 'Times New Roman', serif; transform: translate(-50%, -50%); }
.card-hand__panel { position: absolute; left: 8px; right: 8px; top: 50%; display: grid; gap: 1px; text-align: center; font-family: Georgia, 'Times New Roman', serif; opacity: 0; transform: translateY(-50%); pointer-events: none; transition: opacity 0.22s; }
.card-hand__panel b { font-size: 12px; font-weight: 700; }
.card-hand__panel strong { font-size: 12px; font-weight: 700; white-space: nowrap; }
.card-hand__panel i { font-size: 6.5px; font-style: italic; }
.card-hand__card[aria-pressed='true'] .card-hand__panel { opacity: 1; transition-delay: 0.2s; }
.card-hand__card[aria-pressed='true'] .card-hand__pip, .card-hand__card[aria-pressed='true'] .card-hand__big { opacity: 0.08; transition: opacity 0.2s; }
@media (prefers-reduced-motion: reduce) {
.card-hand__card { transition: none; }
}
`;
function Pip({ suit, className, style }: { suit: Suit; className: string; style?: CSSProperties }) {
return (
<svg className={className} viewBox="0 0 24 24" style={style} aria-hidden="true">
<path d={PIPS[suit]} />
</svg>
);
}
export function HandCard() {
const root = useRef<HTMLDivElement>(null);
const [dealt, setDealt] = useState(false);
const [hover, setHover] = useState<number | null>(null);
const [played, setPlayed] = useState<number | null>(null);
// The fan opens the first time it scrolls into view.
useEffect(() => {
const el = root.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
window.setTimeout(() => setDealt(true), 200);
io.disconnect();
}
});
io.observe(el);
return () => io.disconnect();
}, []);
const pose = (i: number): CSSProperties => {
const mid = (CARDS.length - 1) / 2;
if (played === i) return { '--x': '0px', '--y': '-16px', '--a': '0deg', scale: '1.3', zIndex: 20, transformOrigin: '50% 100%' } as CSSProperties;
if (!dealt) return { '--x': '0px', '--y': `${-i * 0.7}px`, '--a': '0deg', transitionDelay: '0ms' } as CSSProperties;
// Rest in an arc; the neighbours of a raised card lean away to show it.
let a = (i - mid) * 12;
if (hover !== null && played === null && i !== hover) a += (i < hover ? -1 : 1) * 7;
const rad = (a * Math.PI) / 180;
const lift = played === null && hover === i ? 22 : 0;
const drop = played !== null ? 74 : 0;
return {
'--x': `${(Math.sin(rad) * (120 + lift)).toFixed(1)}px`,
'--y': `${((1 - Math.cos(rad)) * 120 - lift + drop).toFixed(1)}px`,
'--a': `${(hover === i && played === null ? a * 0.72 : a).toFixed(1)}deg`,
zIndex: i + 1,
transitionDelay: dealt && hover === null && played === null ? `${i * 40}ms` : '0ms',
} as CSSProperties;
};
const shown = hover ?? null;
return (
<>
<style>
{css}
</style>
<div ref={root} className="card-hand" data-played={played !== null} onPointerLeave={() => setHover(null)}>
<p className="card-hand__say" aria-live="polite">
{shown === null ? (
'Pick your hand'
) : (
<>
<b>{CARDS[shown].plan}</b> · {CARDS[shown].price}
</>
)}
</p>
{CARDS.map((c, i) => (
<button
key={c.word}
type="button"
className="card-hand__card"
style={{ ...pose(i), color: c.suit === 'heart' || c.suit === 'diamond' ? 'var(--red)' : 'var(--black)' }}
aria-label={`${c.word}: ${c.plan} plan, ${c.price}`}
aria-pressed={played === i}
onPointerEnter={() => setHover(i)}
onFocus={() => setHover(i)}
onBlur={() => setHover(null)}
onClick={() => setPlayed(played === i ? null : i)}
>
<span className="card-hand__corner card-hand__corner--a" aria-hidden="true">
{c.rank}
<Pip suit={c.suit} className="" />
</span>
<span className="card-hand__corner card-hand__corner--b" aria-hidden="true">
{c.rank}
<Pip suit={c.suit} className="" />
</span>
{LAYOUT[c.rank]?.map(([x, y]) => (
<Pip key={`${x}${y}`} suit={c.suit} className={`card-hand__pip${y > 50 ? ' card-hand__pip--low' : ''}`} style={{ left: `${x}%`, top: `${y}%` }} />
))}
{c.rank === 'A' && <Pip suit={c.suit} className="card-hand__big card-hand__big--ace" />}
{c.rank === 'K' && (
<span className="card-hand__big card-hand__big--king" aria-hidden="true">
K
</span>
)}
<span className="card-hand__panel" aria-hidden="true" style={{ color: 'var(--black)' }}>
<b>{c.plan}</b>
<strong>{c.price}</strong>
<i>{c.note}</i>
</span>
</button>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardHand(el) on the mounted element instead.
<div class="card-hand" data-played="false">
<p class="card-hand__say" aria-live="polite">Pick your hand</p>
<button type="button" class="card-hand__card" style="--x:0px;--y:0px;--a:0deg;transition-delay:0ms;color:var(--black)" aria-label="Two of clubs: Free plan, $0" aria-pressed="false">
<span class="card-hand__corner card-hand__corner--a" aria-hidden="true">
2
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2.5 A5 5 0 0 1 16.6 9.5 A5 5 0 1 1 13.4 17.6 C13.6 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.4 19.4 10.6 17.6 A5 5 0 1 1 7.4 9.5 A5 5 0 0 1 12 2.5 Z"></path>
</svg>
</span>
<span class="card-hand__corner card-hand__corner--b" aria-hidden="true">
2
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2.5 A5 5 0 0 1 16.6 9.5 A5 5 0 1 1 13.4 17.6 C13.6 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.4 19.4 10.6 17.6 A5 5 0 1 1 7.4 9.5 A5 5 0 0 1 12 2.5 Z"></path>
</svg>
</span>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:50%;top:24%" aria-hidden="true">
<path d="M12 2.5 A5 5 0 0 1 16.6 9.5 A5 5 0 1 1 13.4 17.6 C13.6 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.4 19.4 10.6 17.6 A5 5 0 1 1 7.4 9.5 A5 5 0 0 1 12 2.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:50%;top:76%" aria-hidden="true">
<path d="M12 2.5 A5 5 0 0 1 16.6 9.5 A5 5 0 1 1 13.4 17.6 C13.6 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.4 19.4 10.6 17.6 A5 5 0 1 1 7.4 9.5 A5 5 0 0 1 12 2.5 Z"></path>
</svg>
<span class="card-hand__panel" aria-hidden="true" style="color:var(--black)">
<b>Free</b>
<strong>$0</strong>
<i>One project</i>
</span>
</button>
<button type="button" class="card-hand__card" style="--x:0px;--y:-0.7px;--a:0deg;transition-delay:0ms;color:var(--red)" aria-label="Seven of diamonds: Solo plan, $8/mo" aria-pressed="false">
<span class="card-hand__corner card-hand__corner--a" aria-hidden="true">
7
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
</span>
<span class="card-hand__corner card-hand__corner--b" aria-hidden="true">
7
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
</span>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:32%;top:22%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:68%;top:22%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:50%;top:36%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:32%;top:50%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:68%;top:50%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:32%;top:78%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:68%;top:78%" aria-hidden="true">
<path d="M12 2 L20 12 L12 22 L4 12 Z"></path>
</svg>
<span class="card-hand__panel" aria-hidden="true" style="color:var(--black)">
<b>Solo</b>
<strong>$8/mo</strong>
<i>Ten projects</i>
</span>
</button>
<button type="button" class="card-hand__card" style="--x:0px;--y:-1.4px;--a:0deg;transition-delay:0ms;color:var(--red)" aria-label="Ten of hearts: Team plan, $24/mo" aria-pressed="false">
<span class="card-hand__corner card-hand__corner--a" aria-hidden="true">
10
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
</span>
<span class="card-hand__corner card-hand__corner--b" aria-hidden="true">
10
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
</span>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:32%;top:19%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:68%;top:19%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:50%;top:30%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:32%;top:41%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip" viewBox="0 0 24 24" style="left:68%;top:41%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:32%;top:59%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:68%;top:59%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:50%;top:70%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:32%;top:81%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<svg class="card-hand__pip card-hand__pip--low" viewBox="0 0 24 24" style="left:68%;top:81%" aria-hidden="true">
<path d="M12 21.5 C6 16 2.5 12.6 2.5 8.4 C2.5 5.2 5 3 7.8 3 C9.6 3 11.2 4 12 5.6 C12.8 4 14.4 3 16.2 3 C19 3 21.5 5.2 21.5 8.4 C21.5 12.6 18 16 12 21.5 Z"></path>
</svg>
<span class="card-hand__panel" aria-hidden="true" style="color:var(--black)">
<b>Team</b>
<strong>$24/mo</strong>
<i>Five seats</i>
</span>
</button>
<button type="button" class="card-hand__card" style="--x:0px;--y:-2.1px;--a:0deg;transition-delay:0ms;color:var(--black)" aria-label="King of spades: Studio plan, $64/mo" aria-pressed="false">
<span class="card-hand__corner card-hand__corner--a" aria-hidden="true">
K
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z"></path>
</svg>
</span>
<span class="card-hand__corner card-hand__corner--b" aria-hidden="true">
K
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z"></path>
</svg>
</span>
<span class="card-hand__big card-hand__big--king" aria-hidden="true">K</span>
<span class="card-hand__panel" aria-hidden="true" style="color:var(--black)">
<b>Studio</b>
<strong>$64/mo</strong>
<i>Unlimited seats</i>
</span>
</button>
<button type="button" class="card-hand__card" style="--x:0px;--y:-2.8px;--a:0deg;transition-delay:0ms;color:var(--black)" aria-label="Ace of spades: Forever plan, $480 once" aria-pressed="false">
<span class="card-hand__corner card-hand__corner--a" aria-hidden="true">
A
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z"></path>
</svg>
</span>
<span class="card-hand__corner card-hand__corner--b" aria-hidden="true">
A
<svg class="" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z"></path>
</svg>
</span>
<svg class="card-hand__big card-hand__big--ace" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2 C15 7 21 10 21 15 C21 18 18.6 20 16 19.4 C15 19.2 14 18.6 13.3 17.6 C13.5 19.4 14.2 21 15.5 22 L8.5 22 C9.8 21 10.5 19.4 10.7 17.6 C10 18.6 9 19.2 8 19.4 C5.4 20 3 18 3 15 C3 10 9 7 12 2 Z"></path>
</svg>
<span class="card-hand__panel" aria-hidden="true" style="color:var(--black)">
<b>Forever</b>
<strong>$480 once</strong>
<i>Yours for life</i>
</span>
</button>
</div>.card-hand { flex: none; --face: light-dark(#fbfcfd, #f1f2f3); --black: light-dark(#373a40, #121417); --red: color-mix(in oklab, var(--black) 72%, var(--face)); position: relative; width: 270px; height: 226px; }
.card-hand__say { position: absolute; left: 0; right: 0; top: 2px; margin: 0; color: light-dark(#50535b, #c6c8cd); font-size: 13px; text-align: center; transition: opacity 0.2s; }
.card-hand__say b { color: light-dark(#373a40, #f1f2f3); font-weight: 700; }
.card-hand[data-played='true'] .card-hand__say { opacity: 0; }
.card-hand__card { --a: 0deg; position: absolute; left: 50%; bottom: 34px; width: 84px; height: 118px; margin-left: -42px; padding: 0; border: 0; border-radius: 8px; background: var(--face); color: var(--ink); font: inherit; cursor: pointer; box-shadow: -2px 2px 7px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.32)), 0 0 0 1px light-dark(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.1)); transform-origin: 50% 100%; transform: translate(var(--x), var(--y)) rotate(var(--a)); transition: transform 0.42s cubic-bezier(0.3, 1.25, 0.5, 1); }
.card-hand__card:focus-visible { outline: 2px solid light-dark(#373a40, #f1f2f3); outline-offset: 2px; }
.card-hand__corner { position: absolute; display: flex; flex-direction: column; align-items: center; width: 16px; font: 700 15px/1 Georgia, 'Times New Roman', serif; letter-spacing: -0.05em; }
.card-hand__corner svg { width: 11px; height: 11px; margin-top: 1px; fill: currentColor; }
.card-hand__corner--a { left: 5px; top: 6px; }
.card-hand__corner--b { right: 5px; bottom: 6px; transform: rotate(180deg); }
.card-hand__pip { position: absolute; width: 15px; height: 15px; margin: -7.5px; fill: currentColor; }
.card-hand__pip--low { transform: rotate(180deg); }
.card-hand__big { position: absolute; left: 50%; top: 50%; fill: currentColor; }
.card-hand__big--ace { width: 44px; height: 44px; margin: -22px; }
.card-hand__big--king { display: block; font: 700 54px/1 Georgia, 'Times New Roman', serif; transform: translate(-50%, -50%); }
.card-hand__panel { position: absolute; left: 8px; right: 8px; top: 50%; display: grid; gap: 1px; text-align: center; font-family: Georgia, 'Times New Roman', serif; opacity: 0; transform: translateY(-50%); pointer-events: none; transition: opacity 0.22s; }
.card-hand__panel b { font-size: 12px; font-weight: 700; }
.card-hand__panel strong { font-size: 12px; font-weight: 700; white-space: nowrap; }
.card-hand__panel i { font-size: 6.5px; font-style: italic; }
.card-hand__card[aria-pressed='true'] .card-hand__panel { opacity: 1; transition-delay: 0.2s; }
.card-hand__card[aria-pressed='true'] .card-hand__pip, .card-hand__card[aria-pressed='true'] .card-hand__big { opacity: 0.08; transition: opacity 0.2s; }
@media (prefers-reduced-motion: reduce) {
.card-hand__card { transition: none; }
}function initCardHand(root) {
var PLANS = [['Free', '$0'], ['Solo', '$8/mo'], ['Team', '$24/mo'], ['Studio', '$64/mo'], ['Forever', '$480 once']];
var cards = root.querySelectorAll('.card-hand__card');
var say = root.querySelector('.card-hand__say');
var mid = (cards.length - 1) / 2;
var dealt = false;
var hover = null;
var played = null;
var timer = 0;
function paint() {
root.setAttribute('data-played', String(played !== null));
cards.forEach(function (card, i) {
var s = card.style;
var x = 0;
var y = -i * 0.7;
var turn = 0;
s.scale = '';
s.transitionDelay = '0ms';
s.zIndex = '';
if (played === i) {
y = -16;
s.scale = '1.3';
s.zIndex = '20';
} else if (dealt) {
// Rest in an arc; the neighbours of a raised card lean away to show it.
var a = (i - mid) * 12;
if (hover !== null && played === null && i !== hover) a += (i < hover ? -1 : 1) * 7;
var rad = (a * Math.PI) / 180;
var lift = played === null && hover === i ? 22 : 0;
x = Math.sin(rad) * (120 + lift);
y = (1 - Math.cos(rad)) * 120 - lift + (played !== null ? 74 : 0);
turn = hover === i && played === null ? a * 0.72 : a;
s.zIndex = String(i + 1);
if (hover === null && played === null) s.transitionDelay = i * 40 + 'ms';
}
s.setProperty('--x', x.toFixed(1) + 'px');
s.setProperty('--y', y.toFixed(1) + 'px');
s.setProperty('--a', turn.toFixed(1) + 'deg');
card.setAttribute('aria-pressed', String(played === i));
});
say.textContent = '';
if (hover === null) {
say.textContent = 'Pick your hand';
} else {
var name = document.createElement('b');
name.textContent = PLANS[hover][0];
say.appendChild(name);
say.appendChild(document.createTextNode(' · ' + PLANS[hover][1]));
}
}
// The fan opens the first time it scrolls into view.
var io = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
timer = setTimeout(function () {
dealt = true;
paint();
}, 200);
io.disconnect();
}
});
function cardOf(e) {
var card = e.target.closest('.card-hand__card');
return card ? [].indexOf.call(cards, card) : -1;
}
function onOver(e) {
var i = cardOf(e);
if (i >= 0 && i !== hover) {
hover = i;
paint();
}
}
function onLeave() {
hover = null;
paint();
}
function onClick(e) {
var i = cardOf(e);
if (i < 0) return;
played = played === i ? null : i;
paint();
}
io.observe(root);
root.addEventListener('pointerover', onOver);
root.addEventListener('pointerleave', onLeave);
root.addEventListener('focusin', onOver);
root.addEventListener('focusout', onLeave);
root.addEventListener('click', onClick);
return function () {
io.disconnect();
clearTimeout(timer);
root.removeEventListener('pointerover', onOver);
root.removeEventListener('pointerleave', onLeave);
root.removeEventListener('focusin', onOver);
root.removeEventListener('focusout', onLeave);
root.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.card-hand').forEach(initCardHand);Goal: Create a "Hand of cards" card.
Visual behavior: Pricing as an actual hand of playing cards, from the two of clubs (Free) to the ace of spades (Forever). They deal into a fan, rise under your cursor, and the one you pick is played to the table.
Technique:
- Each card is placed by position and angle variables worked out from its place in the arc. The hovered card lifts and straightens while its neighbours lean away, and an overshooting transition carries them all.
- The fan opens with a stagger the first time it scrolls into view. Pips follow the layout of real playing cards, and the card played to the table scales up to show its price.
Accessibility: Each card is a button with aria-pressed and an aria-label giving the card, plan and price, and a live line above repeats the plan under the pointer or focus. Under prefers-reduced-motion the cards jump to their places.
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 "Hand of cards" card.
Visual behavior: Pricing as an actual hand of playing cards, from the two of clubs (Free) to the ace of spades (Forever). They deal into a fan, rise under your cursor, and the one you pick is played to the table.
Technique:
- Each card is placed by position and angle variables worked out from its place in the arc. The hovered card lifts and straightens while its neighbours lean away, and an overshooting transition carries them all.
- The fan opens with a stagger the first time it scrolls into view. Pips follow the layout of real playing cards, and the card played to the table scales up to show its price.
Accessibility: Each card is a button with aria-pressed and an aria-label giving the card, plan and price, and a live line above repeats the plan under the pointer or focus. Under prefers-reduced-motion the cards jump to their places.
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
Pricing as an actual hand of playing cards, from the two of clubs (Free) to the ace of spades (Forever). They deal into a fan, rise under your cursor, and the one you pick is played to the table.
Each card is a button with aria-pressed and an aria-label giving the card, plan and price, and a live line above repeats the plan under the pointer or focus. Under prefers-reduced-motion the cards jump to their places.