Table for two Fri · 8:30 pm
EmberKitchen · Since 1962
import { useState } from 'react';
const FORTUNES = [
{ text: 'Small steps, taken often, go far.', nums: '4 · 17 · 23' },
{ text: 'The idea you dropped is worth a second look.', nums: '8 · 12 · 31' },
{ text: 'Ship it on Tuesday. Polish it on Wednesday.', nums: '2 · 19 · 40' },
];
const CRUMBS = [
{ x: -14, d: 0 },
{ x: -4, d: 0.05 },
{ x: 8, d: 0.02 },
{ x: 16, d: 0.08 },
];
const css = `
@keyframes card-fortune-crumb { from { transform: translate(0, 0); opacity: 1; } to { transform: translate(var(--x), 46px); opacity: 0; } }
.card-fortune {
position: relative;
display: grid;
justify-items: center;
width: 240px;
height: 180px;
padding: 0;
border: none;
background: none;
font: inherit;
cursor: pointer;
}
.card-fortune:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 14px; }
.card-fortune__cookie { position: relative; width: 150px; height: 92px; margin-top: 18px; }
.card-fortune__half { position: absolute; top: 0; width: 76px; height: 92px; overflow: visible; transition: transform 0.55s cubic-bezier(0.3, 1.4, 0.5, 1); }
.card-fortune__half path { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; }
.card-fortune__half .card-fortune__fold { stroke-width: 1.4; stroke-linecap: round; }
.card-fortune__half .card-fortune__fold { fill: none; stroke: light-dark(#6d7179, #7d8087); }
.card-fortune__half--l { left: 0; transform-origin: 100% 80%; }
.card-fortune__half--r { right: 0; transform-origin: 0 80%; }
.card-fortune[aria-pressed='true'] .card-fortune__half--l { transform: translate(-30px, -6px) rotate(-28deg); }
.card-fortune[aria-pressed='true'] .card-fortune__half--r { transform: translate(30px, -6px) rotate(28deg); }
/* The slip pulls out sideways from the crack, a little after it opens. */
.card-fortune__slip {
position: absolute;
left: 50%;
top: 48px;
width: 216px;
padding: 7px 10px;
border-radius: 2px;
background: light-dark(#fbfcfd, #121417);
box-shadow: 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 8px 16px -10px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
color: light-dark(#373a40, #f1f2f3);
font-size: 11.5px;
line-height: 1.35;
text-align: center;
transform: translateX(-50%) scaleX(0.05);
opacity: 0;
transition: transform 0.5s cubic-bezier(0.3, 1.2, 0.5, 1), opacity 0.2s;
}
.card-fortune__slip small { display: block; margin-top: 3px; color: light-dark(#5f626a, #a7abb2); font-size: 9.5px; letter-spacing: 0.08em; }
.card-fortune[aria-pressed='true'] .card-fortune__slip { transform: translateX(-50%) translateY(74px); opacity: 1; transition-delay: 0.22s; }
.card-fortune__crumb { position: absolute; left: 50%; top: 70px; width: 3px; height: 3px; border-radius: 1px; background: light-dark(#6d7179, #7d8087); opacity: 0; }
.card-fortune[aria-pressed='true'] .card-fortune__crumb { animation: card-fortune-crumb 0.7s ease-in var(--d) both; }
.card-fortune__hint { position: absolute; bottom: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 11.5px; transition: opacity 0.2s; }
.card-fortune[aria-pressed='true'] .card-fortune__hint { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.card-fortune * { transition-duration: 0.01s !important; animation: none !important; }
}
`;
export function FortuneCard() {
const [n, setN] = useState(0);
const [open, setOpen] = useState(false);
const f = FORTUNES[n % FORTUNES.length]!;
return (
<>
<style>
{css}
</style>
<button
type="button"
className="card-fortune"
aria-pressed={open}
aria-label={open ? `${f.text} Lucky ${f.nums}. Press for another fortune.` : 'Tap to crack the fortune cookie'}
onClick={() => {
if (open) setN((k) => k + 1);
setOpen((o) => !o);
}}
>
<span className="card-fortune__cookie" aria-hidden="true">
<svg className="card-fortune__half card-fortune__half--l" viewBox="0 0 76 92">
<path d="M75 34C62 12 30 8 14 20C0 32 2 58 16 70C32 84 58 86 75 80Z" />
<path className="card-fortune__fold" d="M70 44C54 40 34 46 28 62" />
</svg>
<svg className="card-fortune__half card-fortune__half--r" viewBox="0 0 76 92">
<path d="M1 34C14 12 46 8 62 20C76 32 74 58 60 70C44 84 18 86 1 80Z" />
<path className="card-fortune__fold" d="M6 44C22 40 42 46 48 62" />
</svg>
{CRUMBS.map((c, i) => (
<span key={i} className="card-fortune__crumb" style={{ '--x': `${c.x}px`, '--d': `${0.12 + c.d}s` } as React.CSSProperties} />
))}
</span>
<span className="card-fortune__slip" aria-hidden="true">
{f.text}
<small>LUCKY {f.nums}</small>
</span>
<span className="card-fortune__hint" aria-hidden="true">
Tap to crack
</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardFortune(el) on the mounted element instead.
<button type="button" class="card-fortune" aria-pressed="false" aria-label="Tap to crack the fortune cookie">
<span class="card-fortune__cookie" aria-hidden="true">
<svg class="card-fortune__half card-fortune__half--l" viewBox="0 0 76 92">
<path d="M75 34C62 12 30 8 14 20C0 32 2 58 16 70C32 84 58 86 75 80Z"></path>
<path class="card-fortune__fold" d="M70 44C54 40 34 46 28 62"></path>
</svg>
<svg class="card-fortune__half card-fortune__half--r" viewBox="0 0 76 92">
<path d="M1 34C14 12 46 8 62 20C76 32 74 58 60 70C44 84 18 86 1 80Z"></path>
<path class="card-fortune__fold" d="M6 44C22 40 42 46 48 62"></path>
</svg>
<span class="card-fortune__crumb" style="--x:-14px;--d:0.12s"></span>
<span class="card-fortune__crumb" style="--x:-4px;--d:0.16999999999999998s"></span>
<span class="card-fortune__crumb" style="--x:8px;--d:0.13999999999999999s"></span>
<span class="card-fortune__crumb" style="--x:16px;--d:0.2s"></span>
</span>
<span class="card-fortune__slip" aria-hidden="true">Small steps, taken often, go far.<small>LUCKY 4 · 17 · 23</small></span>
<span class="card-fortune__hint" aria-hidden="true">Tap to crack</span>
</button>@keyframes card-fortune-crumb { from { transform: translate(0, 0); opacity: 1; } to { transform: translate(var(--x), 46px); opacity: 0; } }
.card-fortune {
position: relative;
display: grid;
justify-items: center;
width: 240px;
height: 180px;
padding: 0;
border: none;
background: none;
font: inherit;
cursor: pointer;
}
.card-fortune:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 14px; }
.card-fortune__cookie { position: relative; width: 150px; height: 92px; margin-top: 18px; }
.card-fortune__half { position: absolute; top: 0; width: 76px; height: 92px; overflow: visible; transition: transform 0.55s cubic-bezier(0.3, 1.4, 0.5, 1); }
.card-fortune__half path { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.2; }
.card-fortune__half .card-fortune__fold { stroke-width: 1.4; stroke-linecap: round; }
.card-fortune__half .card-fortune__fold { fill: none; stroke: light-dark(#6d7179, #7d8087); }
.card-fortune__half--l { left: 0; transform-origin: 100% 80%; }
.card-fortune__half--r { right: 0; transform-origin: 0 80%; }
.card-fortune[aria-pressed='true'] .card-fortune__half--l { transform: translate(-30px, -6px) rotate(-28deg); }
.card-fortune[aria-pressed='true'] .card-fortune__half--r { transform: translate(30px, -6px) rotate(28deg); }
/* The slip pulls out sideways from the crack, a little after it opens. */
.card-fortune__slip {
position: absolute;
left: 50%;
top: 48px;
width: 216px;
padding: 7px 10px;
border-radius: 2px;
background: light-dark(#fbfcfd, #121417);
box-shadow: 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 8px 16px -10px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
color: light-dark(#373a40, #f1f2f3);
font-size: 11.5px;
line-height: 1.35;
text-align: center;
transform: translateX(-50%) scaleX(0.05);
opacity: 0;
transition: transform 0.5s cubic-bezier(0.3, 1.2, 0.5, 1), opacity 0.2s;
}
.card-fortune__slip small { display: block; margin-top: 3px; color: light-dark(#5f626a, #a7abb2); font-size: 9.5px; letter-spacing: 0.08em; }
.card-fortune[aria-pressed='true'] .card-fortune__slip { transform: translateX(-50%) translateY(74px); opacity: 1; transition-delay: 0.22s; }
.card-fortune__crumb { position: absolute; left: 50%; top: 70px; width: 3px; height: 3px; border-radius: 1px; background: light-dark(#6d7179, #7d8087); opacity: 0; }
.card-fortune[aria-pressed='true'] .card-fortune__crumb { animation: card-fortune-crumb 0.7s ease-in var(--d) both; }
.card-fortune__hint { position: absolute; bottom: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 11.5px; transition: opacity 0.2s; }
.card-fortune[aria-pressed='true'] .card-fortune__hint { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.card-fortune * { transition-duration: 0.01s !important; animation: none !important; }
}function initCardFortune(root) {
var FORTUNES = [
{ text: 'Small steps, taken often, go far.', nums: '4 · 17 · 23' },
{ text: 'The idea you dropped is worth a second look.', nums: '8 · 12 · 31' },
{ text: 'Ship it on Tuesday. Polish it on Wednesday.', nums: '2 · 19 · 40' },
];
var slip = root.querySelector('.card-fortune__slip');
var n = 0;
var open = false;
function onClick() {
// Pressing an open cookie closes it and loads the next fortune for the next crack.
if (open) n += 1;
open = !open;
var f = FORTUNES[n % FORTUNES.length];
slip.firstChild.nodeValue = f.text;
slip.querySelector('small').textContent = 'LUCKY ' + f.nums;
root.setAttribute('aria-pressed', String(open));
// The name starts with the words on screen: the hint while closed, the slip once open.
root.setAttribute('aria-label', open ? f.text + ' Lucky ' + f.nums + '. Press for another fortune.' : 'Tap to crack the fortune cookie');
}
root.addEventListener('click', onClick);
return function () { root.removeEventListener('click', onClick); };
}
document.querySelectorAll('.card-fortune').forEach(initCardFortune);Goal: Create a "Fortune" card.
Visual behavior: A fortune cookie card: click it and the cookie cracks in two, crumbs fall and the slip of paper slides out with your fortune and lucky numbers; click again for another.
Technique:
- It is a toggle button (aria-pressed); pressed, each SVG half of the cookie translates and rotates apart and four crumbs fall to their own --x offsets.
- The slip slides out from the crack after a short delay; closing it loads the next fortune for the next crack.
Accessibility: A real <button> with aria-pressed and an aria-label that starts with the words on screen: the hint while closed, the fortune and lucky numbers once open. Under prefers-reduced-motion the cookie opens almost at once and no crumbs fall.
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 "Fortune" card.
Visual behavior: A fortune cookie card: click it and the cookie cracks in two, crumbs fall and the slip of paper slides out with your fortune and lucky numbers; click again for another.
Technique:
- It is a toggle button (aria-pressed); pressed, each SVG half of the cookie translates and rotates apart and four crumbs fall to their own --x offsets.
- The slip slides out from the crack after a short delay; closing it loads the next fortune for the next crack.
Accessibility: A real <button> with aria-pressed and an aria-label that starts with the words on screen: the hint while closed, the fortune and lucky numbers once open. Under prefers-reduced-motion the cookie opens almost at once and no crumbs fall.
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
A fortune cookie card: click it and the cookie cracks in two, crumbs fall and the slip of paper slides out with your fortune and lucky numbers; click again for another.
A real <button> with aria-pressed and an aria-label that starts with the words on screen: the hint while closed, the fortune and lucky numbers once open. Under prefers-reduced-motion the cookie opens almost at once and no crumbs fall.