Pens
import { useEffect, useId, useRef, useState } from 'react';
const f = (n: number) => n.toFixed(1);
// Pencil: a row of cursive loops (a prolate cycloid).
const LOOPS = (() => {
let d = '';
for (let t = 0; t <= 9 * Math.PI; t += 0.12) d += `${d ? 'L' : 'M'}${f(92 + 4.05 * t - 8 * Math.sin(t))} ${f(40 + 9 * Math.cos(t))}`;
return d;
})();
// Brush: a wave whose width swells under pressure and thins to a hairline at each end.
const WAVE = (() => {
const pts = Array.from({ length: 61 }, (_, i) => {
const t = i / 60;
return { x: 92 + 114 * t, y: 40 + 13 * Math.sin(2 * Math.PI * 1.25 * t + 0.3), w: 0.6 + 7 * Math.pow(Math.sin(Math.PI * t), 0.7) * (0.65 + 0.35 * Math.sin(9 * t)) };
});
const side = (s: number) =>
pts.map((p, i) => {
const a = pts[Math.max(0, i - 1)];
const b = pts[Math.min(pts.length - 1, i + 1)];
const len = Math.hypot(b.x - a.x, b.y - a.y);
return `${f(p.x + (s * (a.y - b.y) * p.w) / 2 / len)} ${f(p.y + (s * (b.x - a.x) * p.w) / 2 / len)}`;
});
const left = side(1);
const right = side(-1).reverse();
return {
line: pts.map((p, i) => `${i ? 'L' : 'M'}${f(p.x)} ${f(p.y)}`).join(''),
shape: `M${left.join('L')}L${right.join('L')}Z`,
};
})();
const ZIGZAG = 'M94 26L206 32L98 42L204 48L102 58L202 62';
const SIGN = 'M92 52C98 30 106 28 106 42C106 58 97 60 101 46C105 34 118 30 117 47C116 60 126 57 131 44C135 34 141 37 139 47C137 58 151 56 157 42S171 30 169 47C167 61 181 50 190 41Q200 32 208 44';
const TOOLS = [
{ label: 'Pencil', line: LOOPS, body: '2B graphite, soft enough to shade.' },
{ label: 'Brush', line: WAVE.line, body: 'Round sable, size 6: press to swell.' },
{ label: 'Marker', line: ZIGZAG, body: 'A chisel tip for the line that matters.' },
{ label: 'Ink', line: SIGN, body: 'A fine steel nib, and ink that dries fast.' },
];
const LEN = 76;
const BASE = [{ x: 19, r: -11 }, { x: 29, r: -3 }, { x: 39, r: 5 }, { x: 49, r: 12 }];
const tipAt = (i: number, raised: boolean) => {
const r = (BASE[i].r * Math.PI) / 180;
return { x: BASE[i].x + LEN * Math.sin(r), y: 142 - LEN * Math.cos(r) - (raised ? 16 : 0) };
};
const pose = (x: number, y: number, r: number) => `translate(${f(x)}px, ${f(y)}px) rotate(${f(r)}deg)`;
const WRITE = -155; // tip down, the barrel leaning back to the right like a writing hand
const T = { lift: 260, fly: 380, write: 1150, back: 380, drop: 260 };
const css = `
.tabs-pens { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); width: 230px; }
.tabs-pens__desk { position: relative; height: 150px; }
.tabs-pens__card { position: absolute; left: 74px; top: 4px; width: 154px; height: 112px; border-radius: 3px; background: var(--paper); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)), 0 8px 16px -8px light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); }
.tabs-pens__ink { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.tabs-pens__stroke { opacity: 0; transition: opacity 0.35s; }
.tabs-pens__stroke[data-on] { opacity: 1; transition: none; }
.tabs-pens__mask { mask-type: alpha; }
.tabs-pens__reveal { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-linecap: round; stroke-linejoin: round; }
.tabs-pens__pencil { fill: none; stroke: color-mix(in oklab, var(--ink) 75%, var(--paper)); stroke-width: 1.4; stroke-linecap: round; stroke-dasharray: 3 0.7 1.4 0.6; }
.tabs-pens__brush { fill: var(--ink); }
.tabs-pens__marker { fill: none; stroke: color-mix(in oklab, var(--ink) 38%, transparent); stroke-width: 8; stroke-linejoin: round; }
.tabs-pens__nib { fill: none; stroke: var(--ink); stroke-width: 1.2; stroke-linecap: round; }
.tabs-pens__blot { fill: var(--ink); }
.tabs-pens__panel { position: absolute; left: 86px; right: 14px; top: 76px; color: light-dark(#50535b, #232529); font-size: 11.5px; line-height: 1.45; }
/* The pot: its back rim behind the pens, its body in front of them. */
.tabs-pens__pot { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.tabs-pens__inside { fill: light-dark(#f3f4f7, #1a1c1f); }
.tabs-pens__s1 { stop-color: light-dark(#dbdde1, #2e3035); }
.tabs-pens__s2 { stop-color: light-dark(#6d7179, #7d8087); }
.tabs-pens__s3 { stop-color: light-dark(#ebedf0, #232529); }
.tabs-pens__slit { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 0.6; }
.tabs-pens__rim { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.5; }
.tabs-pens__tool { position: absolute; left: -5px; top: 0; width: 10px; height: ${LEN}px; transform-origin: 5px 0; transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1); pointer-events: none; }
.tabs-pens__tool svg { display: block; overflow: visible; }
.tabs-pens__wood { fill: light-dark(#dbdde1, #a7abb2); }
.tabs-pens__lead { fill: light-dark(#373a40, #f1f2f3); }
.tabs-pens__a { fill: light-dark(#6d7179, #7d8087); }
.tabs-pens__b { fill: light-dark(#cbced3, #393c41); }
.tabs-pens__c { fill: light-dark(#50535b, #c6c8cd); }
.tabs-pens__d { fill: light-dark(#42454c, #e5e6e9); }
.tabs-pens__e { fill: light-dark(#dbdde1, #2e3035); }
.tabs-pens__tabs { position: absolute; left: 74px; right: 0; top: 124px; display: flex; justify-content: space-between; }
.tabs-pens__tab { padding: 4px 3px; border: 0; background: none; color: light-dark(#5f626a, #a7abb2); font: 12px/1 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; cursor: pointer; transition: color 0.2s; }
.tabs-pens__tab:hover { color: light-dark(#42454c, #e5e6e9); }
.tabs-pens__tab[aria-selected='true'] { color: light-dark(#373a40, #f1f2f3); text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 4px; }
.tabs-pens__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; border-radius: 3px; }
`;
// Each pen drawn upright with its writing tip at 5,0.
const DRAWINGS = [
<>
<path className="tabs-pens__wood" d="M5 0L8 11H2Z" />
<path className="tabs-pens__lead" d="M5 0L6.2 4H3.8Z" />
<rect className="tabs-pens__a" x="2" y="11" width="6" height="53" />
<rect className="tabs-pens__b" x="4.2" y="11" width="1.6" height="53" />
<rect className="tabs-pens__c" x="1.8" y="64" width="6.4" height="5" />
<rect className="tabs-pens__e" x="2" y="69" width="6" height="7" rx="2" />
</>,
<>
<path className="tabs-pens__d" d="M5 0C6.5 4 8.5 8 8.5 12C8.5 14 6.8 15 5 15C3.2 15 1.5 14 1.5 12C1.5 8 3.5 4 5 0Z" />
<path className="tabs-pens__c" d="M2 14H8L7.5 27H2.5Z" />
<path className="tabs-pens__a" d="M2.5 27H7.5L6.5 74Q5 76.5 3.5 74Z" />
</>,
<>
<path className="tabs-pens__d" d="M3.5 0L7 2V7H3Z" />
<rect className="tabs-pens__c" x="2" y="7" width="6" height="5" />
<rect className="tabs-pens__b" x="0.5" y="12" width="9" height="64" rx="3" />
<rect className="tabs-pens__c" x="0.5" y="30" width="9" height="16" />
</>,
<>
<path className="tabs-pens__c" d="M5 0L7.6 10H2.4Z" />
<path className="tabs-pens__slit" d="M5 2.5V8" />
<rect className="tabs-pens__a" x="2.4" y="10" width="5.2" height="10" rx="1" />
<rect className="tabs-pens__e" x="1.5" y="20" width="7" height="56" rx="3.5" />
<rect className="tabs-pens__c" x="7.2" y="26" width="1.6" height="22" rx="0.8" />
</>,
];
export function Preview() {
const id = useId();
const [{ sel, from }, setState] = useState({ sel: 3, from: -1 });
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const tools = useRef<(HTMLSpanElement | null)[]>([]);
const reveals = useRef<(SVGPathElement | null)[]>([]);
const running = useRef<Animation[]>([]);
const pick = (i: number) => setState((s) => (s.sel === i ? s : { sel: i, from: s.sel }));
// The pen lifts out of the pot, flies to the card, writes its line with the tip on the
// stroke the whole way, then flies back and drops in, a little proud of the others.
useEffect(() => {
if (from < 0 || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
running.current.forEach((a) => a.cancel());
const tool = tools.current[sel];
const line = reveals.current[sel];
if (!tool || !line) return;
const total = T.lift + T.fly + T.write + T.back + T.drop;
const L = line.getTotalLength();
const rest = tipAt(sel, false);
const home = tipAt(sel, true);
const r = BASE[sel].r;
const at = (ms: number) => ms / total;
const frames: Keyframe[] = [
{ offset: 0, transform: pose(rest.x, rest.y, r), easing: 'cubic-bezier(0.3, 0, 0.3, 1)' },
{ offset: at(T.lift), transform: pose(rest.x, rest.y - 48, r * 0.3), easing: 'cubic-bezier(0.45, 0, 0.25, 1)' },
];
const N = 60;
for (let k = 0; k <= N; k++) {
const p = line.getPointAtLength((L * k) / N);
frames.push({ offset: at(T.lift + T.fly + (T.write * k) / N), transform: pose(p.x, p.y, WRITE + 3 * Math.sin(k / 4)) });
}
frames[frames.length - 1].easing = 'cubic-bezier(0.45, 0, 0.25, 1)';
frames.push({ offset: at(total - T.drop), transform: pose(home.x, home.y - 40, 0), easing: 'cubic-bezier(0.4, 0, 0.6, 1.4)' });
frames.push({ offset: 1, transform: pose(home.x, home.y, r) });
running.current = [
tool.animate(frames, { duration: total }),
line.animate([{ strokeDasharray: `${L} ${L}`, strokeDashoffset: L }, { strokeDasharray: `${L} ${L}`, strokeDashoffset: 0 }], { delay: T.lift + T.fly, duration: T.write, fill: 'backwards' }),
];
return () => running.current.forEach((a) => a.cancel());
}, [sel, from]);
const n = TOOLS.length;
const onKeyDown = (e: React.KeyboardEvent) => {
const next = { ArrowRight: (sel + 1) % n, ArrowLeft: (sel + n - 1) % n, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
pick(next);
tabs.current[next]?.focus();
};
return (
<>
<style href="kit-tabs-pens" precedence="kit">
{css}
</style>
<div className="tabs-pens">
<div className="tabs-pens__desk">
<div className="tabs-pens__card" aria-hidden="true" />
{TOOLS.map((t, i) => (
<div key={t.label} className="tabs-pens__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} hidden={sel !== i}>
{t.body}
</div>
))}
<svg className="tabs-pens__ink" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<defs>
{TOOLS.map((t, i) => (
<mask key={t.label} className="tabs-pens__mask" id={`${id}-mask-${i}`} maskUnits="userSpaceOnUse" x="0" y="0" width="230" height="150">
<path
ref={(el) => {
reveals.current[i] = el;
}}
className="tabs-pens__reveal"
d={t.line}
strokeWidth={i === 2 ? 14 : 11}
/>
</mask>
))}
</defs>
<g className="tabs-pens__stroke" data-on={sel === 0 ? '' : undefined} mask={`url(#${id}-mask-0)`}>
<path className="tabs-pens__pencil" d={LOOPS} />
<path className="tabs-pens__pencil" d={LOOPS} transform="translate(0.5 0.4)" opacity="0.5" />
</g>
<g className="tabs-pens__stroke" data-on={sel === 1 ? '' : undefined} mask={`url(#${id}-mask-1)`}>
<path className="tabs-pens__brush" d={WAVE.shape} />
</g>
<g className="tabs-pens__stroke" data-on={sel === 2 ? '' : undefined} mask={`url(#${id}-mask-2)`}>
<path className="tabs-pens__marker" d={ZIGZAG} />
</g>
<g className="tabs-pens__stroke" data-on={sel === 3 ? '' : undefined} mask={`url(#${id}-mask-3)`}>
<circle className="tabs-pens__blot" cx="92.4" cy="51" r="1.9" />
<path className="tabs-pens__nib" d={SIGN} />
</g>
</svg>
<svg className="tabs-pens__pot" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<ellipse className="tabs-pens__inside" cx="34" cy="100" rx="25" ry="6" />
</svg>
{TOOLS.map((t, i) => {
const p = tipAt(i, sel === i);
return (
<span
key={t.label}
ref={(el) => {
tools.current[i] = el;
}}
className="tabs-pens__tool"
style={{ transform: pose(p.x, p.y, BASE[i].r) }}
aria-hidden="true"
>
<svg width="10" height={LEN} viewBox={`0 0 10 ${LEN}`}>
{DRAWINGS[i]}
</svg>
</span>
);
})}
<svg className="tabs-pens__pot" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<defs>
<linearGradient id={`${id}-pot`} x1="0" x2="1">
<stop className="tabs-pens__s1" offset="0" />
<stop className="tabs-pens__s2" offset="0.4" />
<stop className="tabs-pens__s3" offset="1" />
</linearGradient>
</defs>
<path className="tabs-pens__body" fill={`url(#${id}-pot)`} d="M9 100A25 6 0 0 0 59 100L56 141Q55 146 50 146H18Q13 146 12 141Z" />
<path className="tabs-pens__rim" d="M9 100A25 6 0 0 0 59 100" />
</svg>
<div className="tabs-pens__tabs" role="tablist" aria-label="Pens" onKeyDown={onKeyDown}>
{TOOLS.map((t, i) => (
<button
key={t.label}
ref={(el) => {
tabs.current[i] = el;
}}
className="tabs-pens__tab"
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-selected={sel === i}
aria-controls={`${id}-panel-${i}`}
tabIndex={sel === i ? 0 : -1}
onClick={() => pick(i)}
>
{t.label}
</button>
))}
</div>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initTabsPens(el) on the mounted element instead.
<div class="tabs-pens">
<div class="tabs-pens__desk">
<div class="tabs-pens__card" aria-hidden="true"></div>
<div class="tabs-pens__panel" role="tabpanel" id="tabs-pens-panel-0" aria-labelledby="tabs-pens-tab-0" hidden="">2B graphite, soft enough to shade.</div>
<div class="tabs-pens__panel" role="tabpanel" id="tabs-pens-panel-1" aria-labelledby="tabs-pens-tab-1" hidden="">Round sable, size 6: press to swell.</div>
<div class="tabs-pens__panel" role="tabpanel" id="tabs-pens-panel-2" aria-labelledby="tabs-pens-tab-2" hidden="">A chisel tip for the line that matters.</div>
<div class="tabs-pens__panel" role="tabpanel" id="tabs-pens-panel-3" aria-labelledby="tabs-pens-tab-3">A fine steel nib, and ink that dries fast.</div>
<svg class="tabs-pens__ink" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<defs>
<mask class="tabs-pens__mask" id="tabs-pens-mask-0" maskUnits="userSpaceOnUse" x="0" y="0" width="230" height="150">
<path class="tabs-pens__reveal" d="M92.0 49.0L91.5 48.9L91.1 48.7L90.6 48.4L90.2 48.0L89.9 47.4L89.6 46.8L89.4 46.0L89.3 45.2L89.3 44.2L89.4 43.3L89.6 42.2L89.9 41.2L90.3 40.1L90.9 39.0L91.5 38.0L92.3 36.9L93.1 35.9L94.1 35.0L95.2 34.1L96.3 33.4L97.5 32.7L98.8 32.1L100.2 31.6L101.6 31.3L103.0 31.1L104.5 31.0L105.9 31.0L107.3 31.2L108.7 31.5L110.1 31.9L111.4 32.5L112.7 33.1L113.9 33.8L115.0 34.7L116.0 35.6L116.9 36.6L117.7 37.6L118.4 38.6L118.9 39.7L119.4 40.8L119.8 41.9L120.0 42.9L120.1 43.9L120.1 44.8L120.1 45.7L119.9 46.5L119.6 47.2L119.3 47.8L119.0 48.3L118.5 48.6L118.1 48.9L117.6 49.0L117.1 49.0L116.7 48.8L116.2 48.6L115.8 48.2L115.5 47.6L115.2 47.0L115.0 46.3L114.8 45.5L114.8 44.6L114.8 43.6L115.0 42.6L115.2 41.6L115.6 40.5L116.1 39.4L116.7 38.3L117.4 37.3L118.2 36.3L119.2 35.3L120.2 34.4L121.3 33.6L122.5 32.9L123.8 32.3L125.2 31.8L126.5 31.4L128.0 31.2L129.4 31.0L130.8 31.0L132.3 31.1L133.7 31.4L135.1 31.8L136.4 32.3L137.7 32.9L138.9 33.6L140.0 34.4L141.1 35.3L142.0 36.2L142.9 37.2L143.6 38.3L144.2 39.3L144.7 40.4L145.1 41.5L145.4 42.5L145.5 43.5L145.6 44.5L145.5 45.4L145.4 46.2L145.2 47.0L144.9 47.6L144.5 48.1L144.1 48.5L143.7 48.8L143.2 49.0L142.8 49.0L142.3 48.9L141.8 48.7L141.4 48.3L141.0 47.8L140.7 47.3L140.5 46.6L140.3 45.8L140.2 44.9L140.2 44.0L140.3 43.0L140.6 41.9L140.9 40.9L141.4 39.8L141.9 38.7L142.6 37.7L143.4 36.6L144.3 35.7L145.3 34.7L146.4 33.9L147.5 33.2L148.8 32.5L150.1 32.0L151.5 31.5L152.9 31.2L154.3 31.1L155.8 31.0L157.2 31.1L158.6 31.3L160.0 31.6L161.4 32.1L162.7 32.6L163.9 33.3L165.1 34.1L166.2 34.9L167.1 35.9L168.0 36.8L168.8 37.9L169.4 38.9L170.0 40.0L170.4 41.1L170.7 42.2L170.9 43.2L171.0 44.2L171.0 45.1L170.9 45.9L170.7 46.7L170.5 47.4L170.1 47.9L169.7 48.4L169.3 48.7L168.8 48.9L168.4 49.0L167.9 48.9L167.4 48.8L167.0 48.5L166.6 48.0L166.3 47.5L166.0 46.8L165.8 46.1L165.7 45.2L165.7 44.3L165.7 43.3L165.9 42.3L166.2 41.3L166.6 40.2L167.1 39.1L167.8 38.0L168.5 37.0L169.4 36.0L170.4 35.1L171.4 34.2L172.6 33.4L173.8 32.7L175.1 32.2L176.4 31.7L177.8 31.3L179.2 31.1L180.7 31.0L182.1 31.0L183.6 31.2L185.0 31.5L186.4 31.9L187.7 32.4L188.9 33.1L190.1 33.8L191.2 34.6L192.2 35.5L193.2 36.5L194.0 37.5L194.7 38.5L195.2 39.6L195.7 40.7L196.1 41.8L196.3 42.8L196.4 43.8L196.5 44.8L196.4 45.6L196.2 46.4L196.0 47.1L195.7 47.8L195.3 48.2L194.9 48.6L194.5 48.9L194.0 49.0L193.5 49.0L193.1 48.8L192.6 48.6L192.2 48.2L191.8 47.7L191.5 47.1L191.3 46.4L191.2 45.5L191.1 44.7L191.1 43.7L191.3 42.7L191.5 41.6L191.9 40.6L192.4 39.5L193.0 38.4L193.7 37.4L194.5 36.4L195.4 35.4L196.5 34.5L197.6 33.7L198.8 33.0L200.1 32.3L201.4 31.8L202.8 31.4L204.2 31.2L205.6 31.0" stroke-width="11"></path>
</mask>
<mask class="tabs-pens__mask" id="tabs-pens-mask-1" maskUnits="userSpaceOnUse" x="0" y="0" width="230" height="150">
<path class="tabs-pens__reveal" d="M92.0 43.8L93.9 45.4L95.8 46.9L97.7 48.3L99.6 49.5L101.5 50.6L103.4 51.5L105.3 52.2L107.2 52.7L109.1 52.9L111.0 53.0L112.9 52.8L114.8 52.4L116.7 51.8L118.6 51.0L120.5 50.0L122.4 48.8L124.3 47.5L126.2 46.1L128.1 44.5L130.0 42.9L131.9 41.2L133.8 39.5L135.7 37.8L137.6 36.2L139.5 34.6L141.4 33.1L143.3 31.7L145.2 30.5L147.1 29.4L149.0 28.5L150.9 27.8L152.8 27.3L154.7 27.1L156.6 27.0L158.5 27.2L160.4 27.6L162.3 28.2L164.2 29.0L166.1 30.0L168.0 31.2L169.9 32.5L171.8 33.9L173.7 35.5L175.6 37.1L177.5 38.8L179.4 40.5L181.3 42.2L183.2 43.8L185.1 45.4L187.0 46.9L188.9 48.3L190.8 49.5L192.7 50.6L194.6 51.5L196.5 52.2L198.4 52.7L200.3 52.9L202.2 53.0L204.1 52.8L206.0 52.4" stroke-width="11"></path>
</mask>
<mask class="tabs-pens__mask" id="tabs-pens-mask-2" maskUnits="userSpaceOnUse" x="0" y="0" width="230" height="150">
<path class="tabs-pens__reveal" d="M94 26L206 32L98 42L204 48L102 58L202 62" stroke-width="14"></path>
</mask>
<mask class="tabs-pens__mask" id="tabs-pens-mask-3" maskUnits="userSpaceOnUse" x="0" y="0" width="230" height="150">
<path class="tabs-pens__reveal" d="M92 52C98 30 106 28 106 42C106 58 97 60 101 46C105 34 118 30 117 47C116 60 126 57 131 44C135 34 141 37 139 47C137 58 151 56 157 42S171 30 169 47C167 61 181 50 190 41Q200 32 208 44" stroke-width="11"></path>
</mask>
</defs>
<g class="tabs-pens__stroke" mask="url(#tabs-pens-mask-0)">
<path class="tabs-pens__pencil" d="M92.0 49.0L91.5 48.9L91.1 48.7L90.6 48.4L90.2 48.0L89.9 47.4L89.6 46.8L89.4 46.0L89.3 45.2L89.3 44.2L89.4 43.3L89.6 42.2L89.9 41.2L90.3 40.1L90.9 39.0L91.5 38.0L92.3 36.9L93.1 35.9L94.1 35.0L95.2 34.1L96.3 33.4L97.5 32.7L98.8 32.1L100.2 31.6L101.6 31.3L103.0 31.1L104.5 31.0L105.9 31.0L107.3 31.2L108.7 31.5L110.1 31.9L111.4 32.5L112.7 33.1L113.9 33.8L115.0 34.7L116.0 35.6L116.9 36.6L117.7 37.6L118.4 38.6L118.9 39.7L119.4 40.8L119.8 41.9L120.0 42.9L120.1 43.9L120.1 44.8L120.1 45.7L119.9 46.5L119.6 47.2L119.3 47.8L119.0 48.3L118.5 48.6L118.1 48.9L117.6 49.0L117.1 49.0L116.7 48.8L116.2 48.6L115.8 48.2L115.5 47.6L115.2 47.0L115.0 46.3L114.8 45.5L114.8 44.6L114.8 43.6L115.0 42.6L115.2 41.6L115.6 40.5L116.1 39.4L116.7 38.3L117.4 37.3L118.2 36.3L119.2 35.3L120.2 34.4L121.3 33.6L122.5 32.9L123.8 32.3L125.2 31.8L126.5 31.4L128.0 31.2L129.4 31.0L130.8 31.0L132.3 31.1L133.7 31.4L135.1 31.8L136.4 32.3L137.7 32.9L138.9 33.6L140.0 34.4L141.1 35.3L142.0 36.2L142.9 37.2L143.6 38.3L144.2 39.3L144.7 40.4L145.1 41.5L145.4 42.5L145.5 43.5L145.6 44.5L145.5 45.4L145.4 46.2L145.2 47.0L144.9 47.6L144.5 48.1L144.1 48.5L143.7 48.8L143.2 49.0L142.8 49.0L142.3 48.9L141.8 48.7L141.4 48.3L141.0 47.8L140.7 47.3L140.5 46.6L140.3 45.8L140.2 44.9L140.2 44.0L140.3 43.0L140.6 41.9L140.9 40.9L141.4 39.8L141.9 38.7L142.6 37.7L143.4 36.6L144.3 35.7L145.3 34.7L146.4 33.9L147.5 33.2L148.8 32.5L150.1 32.0L151.5 31.5L152.9 31.2L154.3 31.1L155.8 31.0L157.2 31.1L158.6 31.3L160.0 31.6L161.4 32.1L162.7 32.6L163.9 33.3L165.1 34.1L166.2 34.9L167.1 35.9L168.0 36.8L168.8 37.9L169.4 38.9L170.0 40.0L170.4 41.1L170.7 42.2L170.9 43.2L171.0 44.2L171.0 45.1L170.9 45.9L170.7 46.7L170.5 47.4L170.1 47.9L169.7 48.4L169.3 48.7L168.8 48.9L168.4 49.0L167.9 48.9L167.4 48.8L167.0 48.5L166.6 48.0L166.3 47.5L166.0 46.8L165.8 46.1L165.7 45.2L165.7 44.3L165.7 43.3L165.9 42.3L166.2 41.3L166.6 40.2L167.1 39.1L167.8 38.0L168.5 37.0L169.4 36.0L170.4 35.1L171.4 34.2L172.6 33.4L173.8 32.7L175.1 32.2L176.4 31.7L177.8 31.3L179.2 31.1L180.7 31.0L182.1 31.0L183.6 31.2L185.0 31.5L186.4 31.9L187.7 32.4L188.9 33.1L190.1 33.8L191.2 34.6L192.2 35.5L193.2 36.5L194.0 37.5L194.7 38.5L195.2 39.6L195.7 40.7L196.1 41.8L196.3 42.8L196.4 43.8L196.5 44.8L196.4 45.6L196.2 46.4L196.0 47.1L195.7 47.8L195.3 48.2L194.9 48.6L194.5 48.9L194.0 49.0L193.5 49.0L193.1 48.8L192.6 48.6L192.2 48.2L191.8 47.7L191.5 47.1L191.3 46.4L191.2 45.5L191.1 44.7L191.1 43.7L191.3 42.7L191.5 41.6L191.9 40.6L192.4 39.5L193.0 38.4L193.7 37.4L194.5 36.4L195.4 35.4L196.5 34.5L197.6 33.7L198.8 33.0L200.1 32.3L201.4 31.8L202.8 31.4L204.2 31.2L205.6 31.0"></path>
<path class="tabs-pens__pencil" d="M92.0 49.0L91.5 48.9L91.1 48.7L90.6 48.4L90.2 48.0L89.9 47.4L89.6 46.8L89.4 46.0L89.3 45.2L89.3 44.2L89.4 43.3L89.6 42.2L89.9 41.2L90.3 40.1L90.9 39.0L91.5 38.0L92.3 36.9L93.1 35.9L94.1 35.0L95.2 34.1L96.3 33.4L97.5 32.7L98.8 32.1L100.2 31.6L101.6 31.3L103.0 31.1L104.5 31.0L105.9 31.0L107.3 31.2L108.7 31.5L110.1 31.9L111.4 32.5L112.7 33.1L113.9 33.8L115.0 34.7L116.0 35.6L116.9 36.6L117.7 37.6L118.4 38.6L118.9 39.7L119.4 40.8L119.8 41.9L120.0 42.9L120.1 43.9L120.1 44.8L120.1 45.7L119.9 46.5L119.6 47.2L119.3 47.8L119.0 48.3L118.5 48.6L118.1 48.9L117.6 49.0L117.1 49.0L116.7 48.8L116.2 48.6L115.8 48.2L115.5 47.6L115.2 47.0L115.0 46.3L114.8 45.5L114.8 44.6L114.8 43.6L115.0 42.6L115.2 41.6L115.6 40.5L116.1 39.4L116.7 38.3L117.4 37.3L118.2 36.3L119.2 35.3L120.2 34.4L121.3 33.6L122.5 32.9L123.8 32.3L125.2 31.8L126.5 31.4L128.0 31.2L129.4 31.0L130.8 31.0L132.3 31.1L133.7 31.4L135.1 31.8L136.4 32.3L137.7 32.9L138.9 33.6L140.0 34.4L141.1 35.3L142.0 36.2L142.9 37.2L143.6 38.3L144.2 39.3L144.7 40.4L145.1 41.5L145.4 42.5L145.5 43.5L145.6 44.5L145.5 45.4L145.4 46.2L145.2 47.0L144.9 47.6L144.5 48.1L144.1 48.5L143.7 48.8L143.2 49.0L142.8 49.0L142.3 48.9L141.8 48.7L141.4 48.3L141.0 47.8L140.7 47.3L140.5 46.6L140.3 45.8L140.2 44.9L140.2 44.0L140.3 43.0L140.6 41.9L140.9 40.9L141.4 39.8L141.9 38.7L142.6 37.7L143.4 36.6L144.3 35.7L145.3 34.7L146.4 33.9L147.5 33.2L148.8 32.5L150.1 32.0L151.5 31.5L152.9 31.2L154.3 31.1L155.8 31.0L157.2 31.1L158.6 31.3L160.0 31.6L161.4 32.1L162.7 32.6L163.9 33.3L165.1 34.1L166.2 34.9L167.1 35.9L168.0 36.8L168.8 37.9L169.4 38.9L170.0 40.0L170.4 41.1L170.7 42.2L170.9 43.2L171.0 44.2L171.0 45.1L170.9 45.9L170.7 46.7L170.5 47.4L170.1 47.9L169.7 48.4L169.3 48.7L168.8 48.9L168.4 49.0L167.9 48.9L167.4 48.8L167.0 48.5L166.6 48.0L166.3 47.5L166.0 46.8L165.8 46.1L165.7 45.2L165.7 44.3L165.7 43.3L165.9 42.3L166.2 41.3L166.6 40.2L167.1 39.1L167.8 38.0L168.5 37.0L169.4 36.0L170.4 35.1L171.4 34.2L172.6 33.4L173.8 32.7L175.1 32.2L176.4 31.7L177.8 31.3L179.2 31.1L180.7 31.0L182.1 31.0L183.6 31.2L185.0 31.5L186.4 31.9L187.7 32.4L188.9 33.1L190.1 33.8L191.2 34.6L192.2 35.5L193.2 36.5L194.0 37.5L194.7 38.5L195.2 39.6L195.7 40.7L196.1 41.8L196.3 42.8L196.4 43.8L196.5 44.8L196.4 45.6L196.2 46.4L196.0 47.1L195.7 47.8L195.3 48.2L194.9 48.6L194.5 48.9L194.0 49.0L193.5 49.0L193.1 48.8L192.6 48.6L192.2 48.2L191.8 47.7L191.5 47.1L191.3 46.4L191.2 45.5L191.1 44.7L191.1 43.7L191.3 42.7L191.5 41.6L191.9 40.6L192.4 39.5L193.0 38.4L193.7 37.4L194.5 36.4L195.4 35.4L196.5 34.5L197.6 33.7L198.8 33.0L200.1 32.3L201.4 31.8L202.8 31.4L204.2 31.2L205.6 31.0" transform="translate(0.5 0.4)" opacity="0.5"></path>
</g>
<g class="tabs-pens__stroke" mask="url(#tabs-pens-mask-1)">
<path class="tabs-pens__brush" d="M91.8 44.1L93.5 45.9L95.3 47.6L97.1 49.2L98.9 50.6L100.8 51.9L102.7 53.1L104.7 54.0L106.8 54.8L108.9 55.2L111.1 55.4L113.3 55.4L115.5 55.0L117.7 54.4L119.8 53.5L121.9 52.5L123.9 51.2L125.9 49.8L127.9 48.2L129.8 46.5L131.7 44.8L133.5 43.0L135.3 41.2L137.1 39.4L138.9 37.7L140.7 36.0L142.4 34.4L144.2 33.0L146.0 31.7L147.8 30.7L149.5 29.8L151.3 29.1L153.1 28.6L154.8 28.4L156.6 28.4L158.3 28.7L160.0 29.1L161.7 29.8L163.4 30.6L165.1 31.7L166.9 32.9L168.6 34.2L170.4 35.7L172.2 37.3L174.0 39.0L175.8 40.7L177.7 42.4L179.6 44.1L181.6 45.8L183.5 47.4L185.5 48.8L187.6 50.2L189.7 51.4L191.8 52.4L193.9 53.2L196.0 53.7L198.1 54.1L200.2 54.1L202.2 53.9L204.2 53.5L206.1 52.7L205.9 52.1L204.0 52.1L202.2 52.1L200.4 51.8L198.7 51.3L197.0 50.6L195.3 49.8L193.6 48.8L191.9 47.7L190.2 46.4L188.5 45.0L186.7 43.5L184.8 41.9L183.0 40.3L181.1 38.6L179.2 36.9L177.2 35.3L175.2 33.7L173.2 32.1L171.2 30.7L169.1 29.5L167.1 28.3L165.0 27.4L162.9 26.6L160.8 26.1L158.7 25.7L156.6 25.6L154.6 25.7L152.5 26.0L150.5 26.5L148.5 27.2L146.4 28.1L144.4 29.2L142.4 30.4L140.4 31.7L138.3 33.1L136.3 34.7L134.3 36.2L132.3 37.8L130.3 39.4L128.3 41.0L126.4 42.5L124.5 43.9L122.7 45.3L120.9 46.5L119.1 47.5L117.4 48.5L115.7 49.2L114.1 49.8L112.5 50.3L110.9 50.5L109.3 50.7L107.6 50.6L105.9 50.4L104.1 49.9L102.2 49.3L100.3 48.4L98.3 47.4L96.3 46.3L94.3 45.0L92.2 43.6Z"></path>
</g>
<g class="tabs-pens__stroke" mask="url(#tabs-pens-mask-2)">
<path class="tabs-pens__marker" d="M94 26L206 32L98 42L204 48L102 58L202 62"></path>
</g>
<g class="tabs-pens__stroke" data-on="" mask="url(#tabs-pens-mask-3)">
<circle class="tabs-pens__blot" cx="92.4" cy="51" r="1.9"></circle>
<path class="tabs-pens__nib" d="M92 52C98 30 106 28 106 42C106 58 97 60 101 46C105 34 118 30 117 47C116 60 126 57 131 44C135 34 141 37 139 47C137 58 151 56 157 42S171 30 169 47C167 61 181 50 190 41Q200 32 208 44"></path>
</g>
</svg>
<svg class="tabs-pens__pot" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<ellipse class="tabs-pens__inside" cx="34" cy="100" rx="25" ry="6"></ellipse>
</svg>
<span class="tabs-pens__tool" style="transform:translate(4.5px, 67.4px) rotate(-11.0deg)" aria-hidden="true">
<svg width="10" height="76" viewBox="0 0 10 76">
<path class="tabs-pens__wood" d="M5 0L8 11H2Z"></path>
<path class="tabs-pens__lead" d="M5 0L6.2 4H3.8Z"></path>
<rect class="tabs-pens__a" x="2" y="11" width="6" height="53"></rect>
<rect class="tabs-pens__b" x="4.2" y="11" width="1.6" height="53"></rect>
<rect class="tabs-pens__c" x="1.8" y="64" width="6.4" height="5"></rect>
<rect class="tabs-pens__e" x="2" y="69" width="6" height="7" rx="2"></rect>
</svg>
</span>
<span class="tabs-pens__tool" style="transform:translate(25.0px, 66.1px) rotate(-3.0deg)" aria-hidden="true">
<svg width="10" height="76" viewBox="0 0 10 76">
<path class="tabs-pens__d" d="M5 0C6.5 4 8.5 8 8.5 12C8.5 14 6.8 15 5 15C3.2 15 1.5 14 1.5 12C1.5 8 3.5 4 5 0Z"></path>
<path class="tabs-pens__c" d="M2 14H8L7.5 27H2.5Z"></path>
<path class="tabs-pens__a" d="M2.5 27H7.5L6.5 74Q5 76.5 3.5 74Z"></path>
</svg>
</span>
<span class="tabs-pens__tool" style="transform:translate(45.6px, 66.3px) rotate(5.0deg)" aria-hidden="true">
<svg width="10" height="76" viewBox="0 0 10 76">
<path class="tabs-pens__d" d="M3.5 0L7 2V7H3Z"></path>
<rect class="tabs-pens__c" x="2" y="7" width="6" height="5"></rect>
<rect class="tabs-pens__b" x="0.5" y="12" width="9" height="64" rx="3"></rect>
<rect class="tabs-pens__c" x="0.5" y="30" width="9" height="16"></rect>
</svg>
</span>
<span class="tabs-pens__tool" style="transform:translate(64.8px, 51.7px) rotate(12.0deg)" aria-hidden="true">
<svg width="10" height="76" viewBox="0 0 10 76">
<path class="tabs-pens__c" d="M5 0L7.6 10H2.4Z"></path>
<path class="tabs-pens__slit" d="M5 2.5V8"></path>
<rect class="tabs-pens__a" x="2.4" y="10" width="5.2" height="10" rx="1"></rect>
<rect class="tabs-pens__e" x="1.5" y="20" width="7" height="56" rx="3.5"></rect>
<rect class="tabs-pens__c" x="7.2" y="26" width="1.6" height="22" rx="0.8"></rect>
</svg>
</span>
<svg class="tabs-pens__pot" width="230" height="150" viewBox="0 0 230 150" aria-hidden="true">
<defs>
<linearGradient id="tabs-pens-pot" x1="0" x2="1">
<stop class="tabs-pens__s1" offset="0"></stop>
<stop class="tabs-pens__s2" offset="0.4"></stop>
<stop class="tabs-pens__s3" offset="1"></stop>
</linearGradient>
</defs>
<path class="tabs-pens__body" fill="url(#tabs-pens-pot)" d="M9 100A25 6 0 0 0 59 100L56 141Q55 146 50 146H18Q13 146 12 141Z"></path>
<path class="tabs-pens__rim" d="M9 100A25 6 0 0 0 59 100"></path>
</svg>
<div class="tabs-pens__tabs" role="tablist" aria-label="Pens">
<button class="tabs-pens__tab" type="button" role="tab" id="tabs-pens-tab-0" aria-selected="false" aria-controls="tabs-pens-panel-0" tabindex="-1">Pencil</button>
<button class="tabs-pens__tab" type="button" role="tab" id="tabs-pens-tab-1" aria-selected="false" aria-controls="tabs-pens-panel-1" tabindex="-1">Brush</button>
<button class="tabs-pens__tab" type="button" role="tab" id="tabs-pens-tab-2" aria-selected="false" aria-controls="tabs-pens-panel-2" tabindex="-1">Marker</button>
<button class="tabs-pens__tab" type="button" role="tab" id="tabs-pens-tab-3" aria-selected="true" aria-controls="tabs-pens-panel-3" tabindex="0">Ink</button>
</div>
</div>
</div>.tabs-pens { --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); width: 230px; }
.tabs-pens__desk { position: relative; height: 150px; }
.tabs-pens__card { position: absolute; left: 74px; top: 4px; width: 154px; height: 112px; border-radius: 3px; background: var(--paper); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)), 0 8px 16px -8px light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); }
.tabs-pens__ink { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.tabs-pens__stroke { opacity: 0; transition: opacity 0.35s; }
.tabs-pens__stroke[data-on] { opacity: 1; transition: none; }
.tabs-pens__mask { mask-type: alpha; }
.tabs-pens__reveal { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-linecap: round; stroke-linejoin: round; }
.tabs-pens__pencil { fill: none; stroke: color-mix(in oklab, var(--ink) 75%, var(--paper)); stroke-width: 1.4; stroke-linecap: round; stroke-dasharray: 3 0.7 1.4 0.6; }
.tabs-pens__brush { fill: var(--ink); }
.tabs-pens__marker { fill: none; stroke: color-mix(in oklab, var(--ink) 38%, transparent); stroke-width: 8; stroke-linejoin: round; }
.tabs-pens__nib { fill: none; stroke: var(--ink); stroke-width: 1.2; stroke-linecap: round; }
.tabs-pens__blot { fill: var(--ink); }
.tabs-pens__panel { position: absolute; left: 86px; right: 14px; top: 76px; color: light-dark(#50535b, #232529); font-size: 11.5px; line-height: 1.45; }
/* The pot: its back rim behind the pens, its body in front of them. */
.tabs-pens__pot { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.tabs-pens__inside { fill: light-dark(#f3f4f7, #1a1c1f); }
.tabs-pens__s1 { stop-color: light-dark(#dbdde1, #2e3035); }
.tabs-pens__s2 { stop-color: light-dark(#6d7179, #7d8087); }
.tabs-pens__s3 { stop-color: light-dark(#ebedf0, #232529); }
.tabs-pens__slit { fill: none; stroke: light-dark(#dbdde1, #2e3035); stroke-width: 0.6; }
.tabs-pens__rim { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.5; }
.tabs-pens__tool { position: absolute; left: -5px; top: 0; width: 10px; height: 76px; transform-origin: 5px 0; transition: transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1); pointer-events: none; }
.tabs-pens__tool svg { display: block; overflow: visible; }
.tabs-pens__wood { fill: light-dark(#dbdde1, #a7abb2); }
.tabs-pens__lead { fill: light-dark(#373a40, #f1f2f3); }
.tabs-pens__a { fill: light-dark(#6d7179, #7d8087); }
.tabs-pens__b { fill: light-dark(#cbced3, #393c41); }
.tabs-pens__c { fill: light-dark(#50535b, #c6c8cd); }
.tabs-pens__d { fill: light-dark(#42454c, #e5e6e9); }
.tabs-pens__e { fill: light-dark(#dbdde1, #2e3035); }
.tabs-pens__tabs { position: absolute; left: 74px; right: 0; top: 124px; display: flex; justify-content: space-between; }
.tabs-pens__tab { padding: 4px 3px; border: 0; background: none; color: light-dark(#5f626a, #a7abb2); font: 12px/1 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; cursor: pointer; transition: color 0.2s; }
.tabs-pens__tab:hover { color: light-dark(#42454c, #e5e6e9); }
.tabs-pens__tab[aria-selected='true'] { color: light-dark(#373a40, #f1f2f3); text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 4px; }
.tabs-pens__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; border-radius: 3px; }function initTabsPens(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 tools = root.querySelectorAll('.tabs-pens__tool');
var reveals = root.querySelectorAll('.tabs-pens__reveal');
var strokes = root.querySelectorAll('.tabs-pens__stroke');
var LEN = 76;
var BASE = [{ x: 19, r: -11 }, { x: 29, r: -3 }, { x: 39, r: 5 }, { x: 49, r: 12 }];
var WRITE = -155; // tip down, the barrel leaning back to the right like a writing hand
var T = { lift: 260, fly: 380, write: 1150, back: 380, drop: 260 };
var running = [];
function f(v) { return v.toFixed(1); }
function tipAt(i, raised) {
var r = (BASE[i].r * Math.PI) / 180;
return { x: BASE[i].x + LEN * Math.sin(r), y: 142 - LEN * Math.cos(r) - (raised ? 16 : 0) };
}
function pose(x, y, r) { return 'translate(' + f(x) + 'px, ' + f(y) + 'px) rotate(' + f(r) + 'deg)'; }
function stop() {
running.forEach(function (a) { a.cancel(); });
running = [];
}
// The pen lifts out of the pot, flies to the card, writes its line with the tip on the
// stroke the whole way, then flies back and drops in, a little proud of the others.
function update() {
stop();
strokes.forEach(function (stroke, k) { stroke.toggleAttribute('data-on', k === sel); });
tools.forEach(function (tool, k) {
var p = tipAt(k, k === sel);
tool.style.transform = pose(p.x, p.y, BASE[k].r);
});
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var line = reveals[sel];
var total = T.lift + T.fly + T.write + T.back + T.drop;
var L = line.getTotalLength();
var rest = tipAt(sel, false);
var home = tipAt(sel, true);
var r = BASE[sel].r;
function at(ms) { return ms / total; }
var frames = [
{ offset: 0, transform: pose(rest.x, rest.y, r), easing: 'cubic-bezier(0.3, 0, 0.3, 1)' },
{ offset: at(T.lift), transform: pose(rest.x, rest.y - 48, r * 0.3), easing: 'cubic-bezier(0.45, 0, 0.25, 1)' },
];
var N = 60;
for (var k = 0; k <= N; k++) {
var p = line.getPointAtLength((L * k) / N);
frames.push({ offset: at(T.lift + T.fly + (T.write * k) / N), transform: pose(p.x, p.y, WRITE + 3 * Math.sin(k / 4)) });
}
frames[frames.length - 1].easing = 'cubic-bezier(0.45, 0, 0.25, 1)';
frames.push({ offset: at(total - T.drop), transform: pose(home.x, home.y - 40, 0), easing: 'cubic-bezier(0.4, 0, 0.6, 1.4)' });
frames.push({ offset: 1, transform: pose(home.x, home.y, r) });
running = [
tools[sel].animate(frames, { duration: total }),
line.animate(
[
{ strokeDasharray: L + ' ' + L, strokeDashoffset: L },
{ strokeDasharray: L + ' ' + L, strokeDashoffset: 0 },
],
{ delay: T.lift + T.fly, duration: T.write, fill: 'backwards' },
),
];
}
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, ArrowLeft: (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);
stop();
};
}
document.querySelectorAll('.tabs-pens').forEach(initTabsPens);Goal: Create a "Pens" tab.
Visual behavior: A pot of pens: the one you pick lifts out, flies to the card and writes a line in its own stroke (grainy pencil, swelling brush, flat marker, fine ink), then drops back in.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- The pen flies on one long Web Animations keyframe list: out of the pot, across to the start of the stroke, then 60 frames sampled along the stroke with getPointAtLength, so the tip stays on the line as it is drawn.
- Each stroke is revealed through a mask whose path is dashed and undrawn in time with the pen, so the visible stroke can be anything: broken dashes for pencil, a filled swelling shape for the brush, a wide see-through line for the marker.
- The pot is drawn in two parts, its inside behind the pens and its body in front, so they really sit in it.
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. The drawing is aria-hidden. Under prefers-reduced-motion the line is written already and the pen does not leave the pot.
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 "Pens" tab.
Visual behavior: A pot of pens: the one you pick lifts out, flies to the card and writes a line in its own stroke (grainy pencil, swelling brush, flat marker, fine ink), then drops back in.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- The pen flies on one long Web Animations keyframe list: out of the pot, across to the start of the stroke, then 60 frames sampled along the stroke with getPointAtLength, so the tip stays on the line as it is drawn.
- Each stroke is revealed through a mask whose path is dashed and undrawn in time with the pen, so the visible stroke can be anything: broken dashes for pencil, a filled swelling shape for the brush, a wide see-through line for the marker.
- The pot is drawn in two parts, its inside behind the pens and its body in front, so they really sit in it.
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. The drawing is aria-hidden. Under prefers-reduced-motion the line is written already and the pen does not leave the pot.
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 pot of pens: the one you pick lifts out, flies to the card and writes a line in its own stroke (grainy pencil, swelling brush, flat marker, fine ink), then drops back in.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- click, state change, draw
- Techniques
- svg, stroke dashoffset, state machine, keyframes
- 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. The drawing is aria-hidden. Under prefers-reduced-motion the line is written already and the pen does not leave the pot.