Fortune Teller
import { useId, useRef, useState } from 'react';
// Four flaps, each hinged on one edge of the square and folded in to the centre.
const FLAPS = [
{ label: 'Love', side: 't', fortune: 'Say it first.', icon: 'M0 4C-6-1-6-6-3-6C-1.5-6 0-5 0-3.5C0-5 1.5-6 3-6C6-6 6-1 0 4Z' },
{ label: 'Work', side: 'r', fortune: 'Ask for more.', icon: 'M-6-2H6V5H-6ZM-2.5-2V-4.5H2.5V-2' },
{ label: 'Luck', side: 'b', fortune: 'Turn left.', icon: 'M0-6L1.8-1.9L6.2-1.8L2.8 1L4 5.4L0 2.9L-4 5.4L-2.8 1L-6.2-1.8L-1.8-1.9Z' },
{ label: 'Home', side: 'l', fortune: 'Call home.', icon: 'M-6 0L0-5.5L6 0M-4.2-1.5V5H4.2V-1.5M-1.2 5V1.5H1.2V5' },
];
const S = 104;
const css = `
.tabs-fortune-teller { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); position: relative; width: 230px; height: 210px; font-family: 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; }
.tabs-fortune-teller__sheet { position: absolute; left: ${(230 - S) / 2}px; top: ${(210 - S) / 2}px; width: ${S}px; height: ${S}px; perspective: 520px; }
/* The inside of the paper: creases, and the fortune under each flap. */
.tabs-fortune-teller__inside { position: absolute; inset: 0; background: linear-gradient(45deg, transparent calc(50% - 0.5px), color-mix(in oklab, var(--ink) 16%, transparent) 50%, transparent calc(50% + 0.5px)), linear-gradient(-45deg, transparent calc(50% - 0.5px), color-mix(in oklab, var(--ink) 16%, transparent) 50%, transparent calc(50% + 0.5px)), color-mix(in oklab, var(--paper) 92%, light-dark(#6d7179, #7d8087)); box-shadow: 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
.tabs-fortune-teller__panel { position: absolute; width: 80px; margin-left: -40px; color: var(--ink); font-size: 12.5px; line-height: 1.1; text-align: center; white-space: nowrap; }
.tabs-fortune-teller__panel[hidden] { display: block; visibility: hidden; transition: visibility 0s 0.4s; }
.tabs-fortune-teller__panel[data-side='t'] { left: 50%; top: 12px; }
.tabs-fortune-teller__panel[data-side='b'] { left: 50%; bottom: 12px; }
.tabs-fortune-teller__panel[data-side='l'] { left: 19px; top: 50%; translate: 0 -50%; rotate: -90deg; }
.tabs-fortune-teller__panel[data-side='r'] { left: 85px; top: 50%; translate: 0 -50%; rotate: 90deg; }
/* A flap turns over its hinge and lies open outside the square, a little lifted. */
.tabs-fortune-teller__flap { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.42s cubic-bezier(0.45, 0, 0.55, 1); pointer-events: none; }
.tabs-fortune-teller__flap[data-side='t'] { transform-origin: 50% 0; }
.tabs-fortune-teller__flap[data-side='b'] { transform-origin: 50% 100%; }
.tabs-fortune-teller__flap[data-side='l'] { transform-origin: 0 50%; }
.tabs-fortune-teller__flap[data-side='r'] { transform-origin: 100% 50%; }
.tabs-fortune-teller__flap[data-open] { transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1) 0.2s; }
.tabs-fortune-teller__flap[data-open][data-side='t'] { transform: rotateX(170deg); }
.tabs-fortune-teller__flap[data-open][data-side='b'] { transform: rotateX(-170deg); }
.tabs-fortune-teller__flap[data-open][data-side='l'] { transform: rotateY(-170deg); }
.tabs-fortune-teller__flap[data-open][data-side='r'] { transform: rotateY(170deg); }
.tabs-fortune-teller__face, .tabs-fortune-teller__back { position: absolute; inset: 0; backface-visibility: hidden; }
.tabs-fortune-teller__face { background: var(--paper); }
.tabs-fortune-teller__face[data-side='t'] { clip-path: polygon(0 0, 100% 0, 50% 50%); background: linear-gradient(var(--paper) 10%, color-mix(in oklab, var(--paper) 90%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='b'] { clip-path: polygon(0 100%, 100% 100%, 50% 50%); background: linear-gradient(0deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 86%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='l'] { clip-path: polygon(0 0, 0 100%, 50% 50%); background: linear-gradient(90deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 88%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='r'] { clip-path: polygon(100% 0, 100% 100%, 50% 50%); background: linear-gradient(-90deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 84%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__back { background: color-mix(in oklab, var(--paper) 86%, light-dark(#6d7179, #7d8087)); }
.tabs-fortune-teller__back[data-side='t'] { transform: rotateX(180deg); clip-path: polygon(0 100%, 100% 100%, 50% 50%); }
.tabs-fortune-teller__back[data-side='b'] { transform: rotateX(180deg); clip-path: polygon(0 0, 100% 0, 50% 50%); }
.tabs-fortune-teller__back[data-side='l'] { transform: rotateY(180deg); clip-path: polygon(100% 0, 100% 100%, 50% 50%); }
.tabs-fortune-teller__back[data-side='r'] { transform: rotateY(180deg); clip-path: polygon(0 0, 0 100%, 50% 50%); }
.tabs-fortune-teller__mark { position: absolute; display: grid; justify-items: center; gap: 1px; width: 50px; margin-left: -25px; color: var(--ink); font-size: 13px; line-height: 1; }
.tabs-fortune-teller__mark svg { width: 13px; height: 13px; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linejoin: round; stroke-linecap: round; }
.tabs-fortune-teller__mark[data-side='t'] { left: 50%; top: 9px; }
.tabs-fortune-teller__mark[data-side='b'] { left: 50%; bottom: 9px; }
.tabs-fortune-teller__mark[data-side='l'] { left: 21px; top: 50%; translate: 0 -50%; }
.tabs-fortune-teller__mark[data-side='r'] { left: 83px; top: 50%; translate: 0 -50%; }
/* The tabs are the four triangles, shaped so only the flap itself takes the click. */
.tabs-fortune-teller__tab { position: absolute; inset: 0; padding: 0; border: 0; background: none; cursor: pointer; }
.tabs-fortune-teller__tab[data-side='t'] { clip-path: polygon(0 0, 100% 0, 50% 50%); }
.tabs-fortune-teller__tab[data-side='b'] { clip-path: polygon(0 100%, 100% 100%, 50% 50%); }
.tabs-fortune-teller__tab[data-side='l'] { clip-path: polygon(0 0, 0 100%, 50% 50%); }
.tabs-fortune-teller__tab[data-side='r'] { clip-path: polygon(100% 0, 100% 100%, 50% 50%); }
.tabs-fortune-teller__tab:focus-visible { outline: none; }
.tabs-fortune-teller__rings { position: absolute; inset: -4px; width: ${S + 8}px; height: ${S + 8}px; overflow: visible; pointer-events: none; fill: none; stroke: var(--ink); stroke-width: 2; stroke-linejoin: round; }
.tabs-fortune-teller__rings path { opacity: 0; }
.tabs-fortune-teller__tab[data-side='t']:focus-visible ~ .tabs-fortune-teller__rings [data-side='t'],
.tabs-fortune-teller__tab[data-side='r']:focus-visible ~ .tabs-fortune-teller__rings [data-side='r'],
.tabs-fortune-teller__tab[data-side='b']:focus-visible ~ .tabs-fortune-teller__rings [data-side='b'],
.tabs-fortune-teller__tab[data-side='l']:focus-visible ~ .tabs-fortune-teller__rings [data-side='l'] { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.tabs-fortune-teller__flap, .tabs-fortune-teller__flap[data-open] { transition: none; }
}
`;
const RING = {
t: 'M4 4H108L56 56Z',
r: 'M108 4V108L56 56Z',
b: 'M4 108H108L56 56Z',
l: 'M4 4V108L56 56Z',
};
export function Preview() {
const id = useId();
const [sel, setSel] = useState(0);
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const n = FLAPS.length;
const onKeyDown = (e: React.KeyboardEvent) => {
const next = { ArrowRight: (sel + 1) % n, ArrowDown: (sel + 1) % n, ArrowLeft: (sel + n - 1) % n, ArrowUp: (sel + n - 1) % n, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
setSel(next);
tabs.current[next]?.focus();
};
return (
<>
<style href="kit-tabs-fortune-teller" precedence="kit">
{css}
</style>
<div className="tabs-fortune-teller">
<div className="tabs-fortune-teller__sheet">
<div className="tabs-fortune-teller__inside">
{FLAPS.map((fl, i) => (
<div key={fl.label} className="tabs-fortune-teller__panel" data-side={fl.side} role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} hidden={sel !== i}>
{fl.fortune}
</div>
))}
</div>
{FLAPS.map((fl, i) => (
<span key={fl.label} className="tabs-fortune-teller__flap" data-side={fl.side} data-open={sel === i ? '' : undefined} aria-hidden="true">
<span className="tabs-fortune-teller__face" data-side={fl.side}>
<span className="tabs-fortune-teller__mark" data-side={fl.side}>
{fl.side !== 'b' && (
<svg viewBox="-7 -7 14 14">
<path d={fl.icon} />
</svg>
)}
{fl.label}
{fl.side === 'b' && (
<svg viewBox="-7 -7 14 14">
<path d={fl.icon} />
</svg>
)}
</span>
</span>
<span className="tabs-fortune-teller__back" data-side={fl.side} />
</span>
))}
<div role="tablist" aria-label="Fortunes" onKeyDown={onKeyDown}>
{FLAPS.map((fl, i) => (
<button
key={fl.label}
ref={(el) => {
tabs.current[i] = el;
}}
className="tabs-fortune-teller__tab"
data-side={fl.side}
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-label={fl.label}
aria-selected={sel === i}
aria-controls={`${id}-panel-${i}`}
tabIndex={sel === i ? 0 : -1}
onClick={() => setSel(i)}
/>
))}
<svg className="tabs-fortune-teller__rings" viewBox={`0 0 ${S + 8} ${S + 8}`} aria-hidden="true">
{FLAPS.map((fl) => (
<path key={fl.side} data-side={fl.side} d={RING[fl.side as keyof typeof RING]} />
))}
</svg>
</div>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initTabsFortuneTeller(el) on the mounted element instead.
<div class="tabs-fortune-teller">
<div class="tabs-fortune-teller__sheet">
<div class="tabs-fortune-teller__inside">
<div class="tabs-fortune-teller__panel" data-side="t" role="tabpanel" id="tabs-fortune-teller-panel-0" aria-labelledby="tabs-fortune-teller-tab-0">Say it first.</div>
<div class="tabs-fortune-teller__panel" data-side="r" role="tabpanel" id="tabs-fortune-teller-panel-1" aria-labelledby="tabs-fortune-teller-tab-1" hidden="">Ask for more.</div>
<div class="tabs-fortune-teller__panel" data-side="b" role="tabpanel" id="tabs-fortune-teller-panel-2" aria-labelledby="tabs-fortune-teller-tab-2" hidden="">Turn left.</div>
<div class="tabs-fortune-teller__panel" data-side="l" role="tabpanel" id="tabs-fortune-teller-panel-3" aria-labelledby="tabs-fortune-teller-tab-3" hidden="">Call home.</div>
</div>
<span class="tabs-fortune-teller__flap" data-side="t" data-open="" aria-hidden="true">
<span class="tabs-fortune-teller__face" data-side="t">
<span class="tabs-fortune-teller__mark" data-side="t">
<svg viewBox="-7 -7 14 14">
<path d="M0 4C-6-1-6-6-3-6C-1.5-6 0-5 0-3.5C0-5 1.5-6 3-6C6-6 6-1 0 4Z"></path>
</svg>
Love
</span>
</span>
<span class="tabs-fortune-teller__back" data-side="t"></span>
</span>
<span class="tabs-fortune-teller__flap" data-side="r" aria-hidden="true">
<span class="tabs-fortune-teller__face" data-side="r">
<span class="tabs-fortune-teller__mark" data-side="r">
<svg viewBox="-7 -7 14 14">
<path d="M-6-2H6V5H-6ZM-2.5-2V-4.5H2.5V-2"></path>
</svg>
Work
</span>
</span>
<span class="tabs-fortune-teller__back" data-side="r"></span>
</span>
<span class="tabs-fortune-teller__flap" data-side="b" aria-hidden="true">
<span class="tabs-fortune-teller__face" data-side="b">
<span class="tabs-fortune-teller__mark" data-side="b">
Luck
<svg viewBox="-7 -7 14 14">
<path d="M0-6L1.8-1.9L6.2-1.8L2.8 1L4 5.4L0 2.9L-4 5.4L-2.8 1L-6.2-1.8L-1.8-1.9Z"></path>
</svg>
</span>
</span>
<span class="tabs-fortune-teller__back" data-side="b"></span>
</span>
<span class="tabs-fortune-teller__flap" data-side="l" aria-hidden="true">
<span class="tabs-fortune-teller__face" data-side="l">
<span class="tabs-fortune-teller__mark" data-side="l">
<svg viewBox="-7 -7 14 14">
<path d="M-6 0L0-5.5L6 0M-4.2-1.5V5H4.2V-1.5M-1.2 5V1.5H1.2V5"></path>
</svg>
Home
</span>
</span>
<span class="tabs-fortune-teller__back" data-side="l"></span>
</span>
<div role="tablist" aria-label="Fortunes">
<button class="tabs-fortune-teller__tab" data-side="t" type="button" role="tab" id="tabs-fortune-teller-tab-0" aria-label="Love" aria-selected="true" aria-controls="tabs-fortune-teller-panel-0" tabindex="0"></button>
<button class="tabs-fortune-teller__tab" data-side="r" type="button" role="tab" id="tabs-fortune-teller-tab-1" aria-label="Work" aria-selected="false" aria-controls="tabs-fortune-teller-panel-1" tabindex="-1"></button>
<button class="tabs-fortune-teller__tab" data-side="b" type="button" role="tab" id="tabs-fortune-teller-tab-2" aria-label="Luck" aria-selected="false" aria-controls="tabs-fortune-teller-panel-2" tabindex="-1"></button>
<button class="tabs-fortune-teller__tab" data-side="l" type="button" role="tab" id="tabs-fortune-teller-tab-3" aria-label="Home" aria-selected="false" aria-controls="tabs-fortune-teller-panel-3" tabindex="-1"></button>
<svg class="tabs-fortune-teller__rings" viewBox="0 0 112 112" aria-hidden="true">
<path data-side="t" d="M4 4H108L56 56Z"></path>
<path data-side="r" d="M108 4V108L56 56Z"></path>
<path data-side="b" d="M4 108H108L56 56Z"></path>
<path data-side="l" d="M4 4V108L56 56Z"></path>
</svg>
</div>
</div>
</div>.tabs-fortune-teller { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); position: relative; width: 230px; height: 210px; font-family: 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; }
.tabs-fortune-teller__sheet { position: absolute; left: 63px; top: 53px; width: 104px; height: 104px; perspective: 520px; }
/* The inside of the paper: creases, and the fortune under each flap. */
.tabs-fortune-teller__inside { position: absolute; inset: 0; background: linear-gradient(45deg, transparent calc(50% - 0.5px), color-mix(in oklab, var(--ink) 16%, transparent) 50%, transparent calc(50% + 0.5px)), linear-gradient(-45deg, transparent calc(50% - 0.5px), color-mix(in oklab, var(--ink) 16%, transparent) 50%, transparent calc(50% + 0.5px)), color-mix(in oklab, var(--paper) 92%, light-dark(#6d7179, #7d8087)); box-shadow: 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
.tabs-fortune-teller__panel { position: absolute; width: 80px; margin-left: -40px; color: var(--ink); font-size: 12.5px; line-height: 1.1; text-align: center; white-space: nowrap; }
.tabs-fortune-teller__panel[hidden] { display: block; visibility: hidden; transition: visibility 0s 0.4s; }
.tabs-fortune-teller__panel[data-side='t'] { left: 50%; top: 12px; }
.tabs-fortune-teller__panel[data-side='b'] { left: 50%; bottom: 12px; }
.tabs-fortune-teller__panel[data-side='l'] { left: 19px; top: 50%; translate: 0 -50%; rotate: -90deg; }
.tabs-fortune-teller__panel[data-side='r'] { left: 85px; top: 50%; translate: 0 -50%; rotate: 90deg; }
/* A flap turns over its hinge and lies open outside the square, a little lifted. */
.tabs-fortune-teller__flap { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.42s cubic-bezier(0.45, 0, 0.55, 1); pointer-events: none; }
.tabs-fortune-teller__flap[data-side='t'] { transform-origin: 50% 0; }
.tabs-fortune-teller__flap[data-side='b'] { transform-origin: 50% 100%; }
.tabs-fortune-teller__flap[data-side='l'] { transform-origin: 0 50%; }
.tabs-fortune-teller__flap[data-side='r'] { transform-origin: 100% 50%; }
.tabs-fortune-teller__flap[data-open] { transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1) 0.2s; }
.tabs-fortune-teller__flap[data-open][data-side='t'] { transform: rotateX(170deg); }
.tabs-fortune-teller__flap[data-open][data-side='b'] { transform: rotateX(-170deg); }
.tabs-fortune-teller__flap[data-open][data-side='l'] { transform: rotateY(-170deg); }
.tabs-fortune-teller__flap[data-open][data-side='r'] { transform: rotateY(170deg); }
.tabs-fortune-teller__face, .tabs-fortune-teller__back { position: absolute; inset: 0; backface-visibility: hidden; }
.tabs-fortune-teller__face { background: var(--paper); }
.tabs-fortune-teller__face[data-side='t'] { clip-path: polygon(0 0, 100% 0, 50% 50%); background: linear-gradient(var(--paper) 10%, color-mix(in oklab, var(--paper) 90%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='b'] { clip-path: polygon(0 100%, 100% 100%, 50% 50%); background: linear-gradient(0deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 86%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='l'] { clip-path: polygon(0 0, 0 100%, 50% 50%); background: linear-gradient(90deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 88%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__face[data-side='r'] { clip-path: polygon(100% 0, 100% 100%, 50% 50%); background: linear-gradient(-90deg, var(--paper) 10%, color-mix(in oklab, var(--paper) 84%, light-dark(#6d7179, #7d8087)) 50%); }
.tabs-fortune-teller__back { background: color-mix(in oklab, var(--paper) 86%, light-dark(#6d7179, #7d8087)); }
.tabs-fortune-teller__back[data-side='t'] { transform: rotateX(180deg); clip-path: polygon(0 100%, 100% 100%, 50% 50%); }
.tabs-fortune-teller__back[data-side='b'] { transform: rotateX(180deg); clip-path: polygon(0 0, 100% 0, 50% 50%); }
.tabs-fortune-teller__back[data-side='l'] { transform: rotateY(180deg); clip-path: polygon(100% 0, 100% 100%, 50% 50%); }
.tabs-fortune-teller__back[data-side='r'] { transform: rotateY(180deg); clip-path: polygon(0 0, 0 100%, 50% 50%); }
.tabs-fortune-teller__mark { position: absolute; display: grid; justify-items: center; gap: 1px; width: 50px; margin-left: -25px; color: var(--ink); font-size: 13px; line-height: 1; }
.tabs-fortune-teller__mark svg { width: 13px; height: 13px; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linejoin: round; stroke-linecap: round; }
.tabs-fortune-teller__mark[data-side='t'] { left: 50%; top: 9px; }
.tabs-fortune-teller__mark[data-side='b'] { left: 50%; bottom: 9px; }
.tabs-fortune-teller__mark[data-side='l'] { left: 21px; top: 50%; translate: 0 -50%; }
.tabs-fortune-teller__mark[data-side='r'] { left: 83px; top: 50%; translate: 0 -50%; }
/* The tabs are the four triangles, shaped so only the flap itself takes the click. */
.tabs-fortune-teller__tab { position: absolute; inset: 0; padding: 0; border: 0; background: none; cursor: pointer; }
.tabs-fortune-teller__tab[data-side='t'] { clip-path: polygon(0 0, 100% 0, 50% 50%); }
.tabs-fortune-teller__tab[data-side='b'] { clip-path: polygon(0 100%, 100% 100%, 50% 50%); }
.tabs-fortune-teller__tab[data-side='l'] { clip-path: polygon(0 0, 0 100%, 50% 50%); }
.tabs-fortune-teller__tab[data-side='r'] { clip-path: polygon(100% 0, 100% 100%, 50% 50%); }
.tabs-fortune-teller__tab:focus-visible { outline: none; }
.tabs-fortune-teller__rings { position: absolute; inset: -4px; width: 112px; height: 112px; overflow: visible; pointer-events: none; fill: none; stroke: var(--ink); stroke-width: 2; stroke-linejoin: round; }
.tabs-fortune-teller__rings path { opacity: 0; }
.tabs-fortune-teller__tab[data-side='t']:focus-visible ~ .tabs-fortune-teller__rings [data-side='t'],
.tabs-fortune-teller__tab[data-side='r']:focus-visible ~ .tabs-fortune-teller__rings [data-side='r'],
.tabs-fortune-teller__tab[data-side='b']:focus-visible ~ .tabs-fortune-teller__rings [data-side='b'],
.tabs-fortune-teller__tab[data-side='l']:focus-visible ~ .tabs-fortune-teller__rings [data-side='l'] { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.tabs-fortune-teller__flap, .tabs-fortune-teller__flap[data-open] { transition: none; }
}function initTabsFortuneTeller(root) {
var list = root.querySelector('[role="tablist"]');
var tabs = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
var panels = Array.prototype.slice.call(root.querySelectorAll('[role="tabpanel"]'));
var n = tabs.length;
var sel = tabs.findIndex(function (tab) { return tab.getAttribute('aria-selected') === 'true'; });
var flaps = root.querySelectorAll('.tabs-fortune-teller__flap');
function update() {
flaps.forEach(function (flap, k) { flap.toggleAttribute('data-open', k === sel); });
}
function pick(i) {
if (i === sel) return;
var from = sel;
sel = i;
tabs.forEach(function (tab, k) {
tab.setAttribute('aria-selected', String(k === sel));
tab.tabIndex = k === sel ? 0 : -1;
});
panels.forEach(function (panel, k) { panel.hidden = k !== sel; });
update(from);
}
function onClick(e) {
var i = tabs.indexOf(e.currentTarget);
if (i >= 0) pick(i);
}
function onKey(e) {
var next = { ArrowRight: (sel + 1) % n, ArrowDown: (sel + 1) % n, ArrowLeft: (sel + n - 1) % n, ArrowUp: (sel + n - 1) % n, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
pick(next);
tabs[next].focus();
}
tabs.forEach(function (tab) { tab.addEventListener('click', onClick); });
list.addEventListener('keydown', onKey);
return function () {
tabs.forEach(function (tab) { tab.removeEventListener('click', onClick); });
list.removeEventListener('keydown', onKey);
};
}
document.querySelectorAll('.tabs-fortune-teller').forEach(initTabsFortuneTeller);Goal: Create a "Fortune Teller" tab.
Visual behavior: A paper fortune teller: pick a flap and it unfolds over its crease to show the fortune written inside, while the last one folds back down.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Four triangular flaps, each hinged on one edge of the square, turn over that edge and lie open outside it. Each flap has a front and a back face, clipped to the triangle on the faces because clip-path would flatten the 3D on the flap itself.
- The tabs are transparent buttons clipped to the same triangles, so only the flap takes the click. Since a clip also cuts off an outline, the keyboard focus ring is a separate outline shown with a sibling selector.
- The last fortune stays visible until its flap has shut, by delaying visibility on the hidden panel.
Accessibility: Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. Each flap is named, and all four arrow keys move between them. The drawing is aria-hidden. Under prefers-reduced-motion the flaps open and shut 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 "Fortune Teller" tab.
Visual behavior: A paper fortune teller: pick a flap and it unfolds over its crease to show the fortune written inside, while the last one folds back down.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Four triangular flaps, each hinged on one edge of the square, turn over that edge and lie open outside it. Each flap has a front and a back face, clipped to the triangle on the faces because clip-path would flatten the 3D on the flap itself.
- The tabs are transparent buttons clipped to the same triangles, so only the flap takes the click. Since a clip also cuts off an outline, the keyboard focus ring is a separate outline shown with a sibling selector.
- The last fortune stays visible until its flap has shut, by delaying visibility on the hidden panel.
Accessibility: Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. Each flap is named, and all four arrow keys move between them. The drawing is aria-hidden. Under prefers-reduced-motion the flaps open and shut 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 paper fortune teller: pick a flap and it unfolds over its crease to show the fortune written inside, while the last one folds back down.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, 3d
- Techniques
- 3d transform, perspective, preserve 3d, clip path, css transition
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. Each flap is named, and all four arrow keys move between them. The drawing is aria-hidden. Under prefers-reduced-motion the flaps open and shut at once.