Multitool
import { useId, useRef, useState, type CSSProperties } from 'react';
// Each tool is drawn pointing right from its pivot at 0,0. The blade and saw live at the
// left end of the body and open to the left; the opener and driver at the right end.
const TEETH = (() => {
let d = '';
for (let x = 76, up = true; x > 10; x -= 3, up = !up) d += `L${x} ${up ? 10.5 : 6.5}`;
return d;
})();
const TOOLS = [
{
label: 'Blade',
end: 'l',
len: 88,
shape: 'M-9 0A9 9 0 0 1 0-9H58C70-9 81-6 88-1C78 6 64 9 50 9H0A9 9 0 0 1-9 0Z',
edge: 'M6 3.5H52C63 3.5 75 1.6 86-.6C78 6 64 9 50 9H6Z',
line: 'M16-5.6Q22-7.6 28-5.6',
body: 'A 7 cm blade that locks open, for parcels, rope and apples.',
},
{
label: 'Saw',
end: 'l',
len: 80,
shape: `M-9 0A9 9 0 0 1 0-9H74L80-3L78 6.5${TEETH}L10 6.5L0 9A9 9 0 0 1-9 0Z`,
edge: 'M10 3H75L78 6.5H10Z',
line: 'M6-6.4H72',
body: 'Cuts green wood as thick as your thumb, on the pull stroke.',
},
{
label: 'Opener',
end: 'r',
len: 56,
shape: 'M-9 0A9 9 0 0 1 0-9L44-5Q54-5 56 0L52 8.5L48 3.5L40 5L0 9A9 9 0 0 1-9 0Z',
edge: 'M8 4.5L40 2.6L48 1.6L52 8.5L48 3.5L40 5L8 7.6Z',
line: 'M8-5.8L42-3.6',
body: 'Opens a tin in a few turns, and the notch strips wire.',
},
{
label: 'Driver',
end: 'r',
len: 60,
shape: 'M-9 0A9 9 0 0 1 0-9H26L30-4H36L40-8H56L60-5V5L56 8L0 9A9 9 0 0 1-9 0Z',
edge: 'M54-7.6L60-5V5L54 7.8Z',
line: 'M8-6.4H24',
body: 'A flat 3 mm tip for screws, and a lip that lifts bottle caps.',
},
];
// Where the body sits so the open tool and the body are centred together.
const BODY_X = { l: 96, r: 36 };
const css = `
.tabs-multitool { --shade: light-dark(#373a40, #121417); --steel: light-dark(#6d7179, #a7abb2); width: 230px; }
.tabs-multitool__bench { position: relative; height: 84px; }
.tabs-multitool__body { position: absolute; left: 0; top: 51px; width: 112px; height: 26px; transition: transform 0.42s cubic-bezier(0.65, 0, 0.35, 1) 0.26s; }
/* A tool swings about its pivot. Closing is a quick snap shut; opening waits for the
last tool and the body, then flicks out past straight and settles on the lock. */
.tabs-multitool__tool { position: absolute; top: 1px; height: 24px; transform-origin: 10px 12px; transition: transform 0.3s cubic-bezier(0.55, 0, 0.8, 0.3); }
.tabs-multitool__tool[data-end='l'] { left: 3px; transform: rotate(180deg) scaleX(-1); }
.tabs-multitool__tool[data-end='r'] { left: 89px; transform: rotate(-180deg); }
.tabs-multitool__tool[data-open] { transform: rotate(0deg) scaleX(var(--m)); transition: transform 0.55s cubic-bezier(0.25, 1.5, 0.45, 1) var(--d, 0s); }
.tabs-multitool__tool svg { display: block; overflow: visible; }
.tabs-multitool__steel { fill: var(--steel); }
.tabs-multitool__edge { fill: color-mix(in oklab, var(--steel) 55%, light-dark(#373a40, #f1f2f3)); }
.tabs-multitool__line { fill: none; stroke: color-mix(in oklab, var(--steel) 60%, var(--shade)); stroke-width: 1.4; stroke-linecap: round; }
.tabs-multitool__glint { fill: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent); transform: translateX(-30px) skewX(-24deg); }
.tabs-multitool__tool[data-open] .tabs-multitool__glint { animation: tabs-multitool-glint 0.7s ease-in-out calc(var(--d, 0s) + 0.45s) both; }
/* The scale covers the folded tools, with a rivet at each pivot and a small inlay. */
.tabs-multitool__scale { position: absolute; inset: 0; border-radius: 13px; background: radial-gradient(circle at 13px 13px, var(--steel) 0 3px, color-mix(in oklab, var(--steel) 50%, var(--shade)) 3.6px, transparent 4.2px), radial-gradient(circle at 99px 13px, var(--steel) 0 3px, color-mix(in oklab, var(--steel) 50%, var(--shade)) 3.6px, transparent 4.2px), linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035) 55%, light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), inset 0 -1.5px 0 color-mix(in oklab, var(--shade) 40%, transparent), 0 8px 12px -6px light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.6)); }
.tabs-multitool__inlay { position: absolute; left: 40px; top: 8px; width: 32px; height: 10px; border-radius: 3px; background: linear-gradient(var(--steel), color-mix(in oklab, var(--steel) 70%, var(--shade))); color: light-dark(#ebedf0, #232529); font: 700 6.5px/10px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.12em; text-align: center; }
.tabs-multitool__tabs { display: flex; justify-content: space-between; margin-top: 10px; }
.tabs-multitool__tab { position: relative; padding: 6px 4px 7px; border: 0; background: none; color: light-dark(#5f626a, #a7abb2); font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; transition: color 0.2s; }
.tabs-multitool__tab::before { content: ''; position: absolute; left: 50%; top: 0; width: 1px; height: 3px; background: currentColor; transition: height 0.25s; }
.tabs-multitool__tab:hover { color: light-dark(#42454c, #e5e6e9); }
.tabs-multitool__tab[aria-selected='true'] { color: light-dark(#373a40, #f1f2f3); }
.tabs-multitool__tab[aria-selected='true']::before { height: 5px; width: 3px; translate: -1px 0; border-radius: 1px; }
.tabs-multitool__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; border-radius: 4px; }
.tabs-multitool__panel { min-height: 3em; margin-top: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; line-height: 1.5; }
@keyframes tabs-multitool-glint { from { transform: translateX(-30px) skewX(-24deg); } to { transform: translateX(110px) skewX(-24deg); } }
@media (prefers-reduced-motion: reduce) {
.tabs-multitool__tool, .tabs-multitool__tool[data-open], .tabs-multitool__body { transition: none; }
.tabs-multitool__tool[data-open] .tabs-multitool__glint { animation: none; }
}
`;
export function Preview() {
const id = useId();
const [{ sel, from }, setState] = useState({ sel: 0, from: 0 });
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const pick = (i: number) => setState((s) => (s.sel === i ? s : { sel: i, from: s.sel }));
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();
};
const end = TOOLS[sel].end as 'l' | 'r';
// Open once the last tool is shut, and once the body has slid over if it had to.
const delay = TOOLS[from].end === end ? '0.22s' : '0.62s';
const x = `translateX(${BODY_X[end]}px)`;
return (
<>
<style href="kit-tabs-multitool" precedence="kit">
{css}
</style>
<div className="tabs-multitool">
<div className="tabs-multitool__bench" aria-hidden="true">
<span className="tabs-multitool__body" style={{ transform: x }}>
{TOOLS.map((t, i) => (
<span
key={t.label}
className="tabs-multitool__tool"
data-end={t.end}
data-open={sel === i ? '' : undefined}
style={{ width: t.len + 12, '--m': t.end === 'l' ? -1 : 1, '--d': delay } as CSSProperties}
>
<svg width={t.len + 12} height="24" viewBox={`-10 -12 ${t.len + 12} 24`}>
<defs>
<clipPath id={`${id}-clip-${i}`}>
<path d={t.shape} />
</clipPath>
</defs>
<path className="tabs-multitool__steel" d={t.shape} />
<path className="tabs-multitool__edge" d={t.edge} />
<path className="tabs-multitool__line" d={t.line} />
<g clipPath={`url(#${id}-clip-${i})`}>
<rect className="tabs-multitool__glint" x="0" y="-12" width="10" height="24" />
</g>
</svg>
</span>
))}
<span className="tabs-multitool__scale">
<span className="tabs-multitool__inlay">No.4</span>
</span>
</span>
</div>
<div className="tabs-multitool__tabs" role="tablist" aria-label="Tools" onKeyDown={onKeyDown}>
{TOOLS.map((t, i) => (
<button
key={t.label}
ref={(el) => {
tabs.current[i] = el;
}}
className="tabs-multitool__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>
{TOOLS.map((t, i) => (
<div key={t.label} className="tabs-multitool__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} hidden={sel !== i}>
{t.body}
</div>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initTabsMultitool(el) on the mounted element instead.
<div class="tabs-multitool">
<div class="tabs-multitool__bench" aria-hidden="true">
<span class="tabs-multitool__body" style="transform:translateX(96px)">
<span class="tabs-multitool__tool" data-end="l" data-open="" style="width:100px;--m:-1;--d:0.22s">
<svg width="100" height="24" viewBox="-10 -12 100 24">
<defs>
<clipPath id="tabs-multitool-clip-0">
<path d="M-9 0A9 9 0 0 1 0-9H58C70-9 81-6 88-1C78 6 64 9 50 9H0A9 9 0 0 1-9 0Z"></path>
</clipPath>
</defs>
<path class="tabs-multitool__steel" d="M-9 0A9 9 0 0 1 0-9H58C70-9 81-6 88-1C78 6 64 9 50 9H0A9 9 0 0 1-9 0Z"></path>
<path class="tabs-multitool__edge" d="M6 3.5H52C63 3.5 75 1.6 86-.6C78 6 64 9 50 9H6Z"></path>
<path class="tabs-multitool__line" d="M16-5.6Q22-7.6 28-5.6"></path>
<g clip-path="url(#tabs-multitool-clip-0)">
<rect class="tabs-multitool__glint" x="0" y="-12" width="10" height="24"></rect>
</g>
</svg>
</span>
<span class="tabs-multitool__tool" data-end="l" style="width:92px;--m:-1;--d:0.22s">
<svg width="92" height="24" viewBox="-10 -12 92 24">
<defs>
<clipPath id="tabs-multitool-clip-1">
<path d="M-9 0A9 9 0 0 1 0-9H74L80-3L78 6.5L76 10.5L73 6.5L70 10.5L67 6.5L64 10.5L61 6.5L58 10.5L55 6.5L52 10.5L49 6.5L46 10.5L43 6.5L40 10.5L37 6.5L34 10.5L31 6.5L28 10.5L25 6.5L22 10.5L19 6.5L16 10.5L13 6.5L10 6.5L0 9A9 9 0 0 1-9 0Z"></path>
</clipPath>
</defs>
<path class="tabs-multitool__steel" d="M-9 0A9 9 0 0 1 0-9H74L80-3L78 6.5L76 10.5L73 6.5L70 10.5L67 6.5L64 10.5L61 6.5L58 10.5L55 6.5L52 10.5L49 6.5L46 10.5L43 6.5L40 10.5L37 6.5L34 10.5L31 6.5L28 10.5L25 6.5L22 10.5L19 6.5L16 10.5L13 6.5L10 6.5L0 9A9 9 0 0 1-9 0Z"></path>
<path class="tabs-multitool__edge" d="M10 3H75L78 6.5H10Z"></path>
<path class="tabs-multitool__line" d="M6-6.4H72"></path>
<g clip-path="url(#tabs-multitool-clip-1)">
<rect class="tabs-multitool__glint" x="0" y="-12" width="10" height="24"></rect>
</g>
</svg>
</span>
<span class="tabs-multitool__tool" data-end="r" style="width:68px;--m:1;--d:0.22s">
<svg width="68" height="24" viewBox="-10 -12 68 24">
<defs>
<clipPath id="tabs-multitool-clip-2">
<path d="M-9 0A9 9 0 0 1 0-9L44-5Q54-5 56 0L52 8.5L48 3.5L40 5L0 9A9 9 0 0 1-9 0Z"></path>
</clipPath>
</defs>
<path class="tabs-multitool__steel" d="M-9 0A9 9 0 0 1 0-9L44-5Q54-5 56 0L52 8.5L48 3.5L40 5L0 9A9 9 0 0 1-9 0Z"></path>
<path class="tabs-multitool__edge" d="M8 4.5L40 2.6L48 1.6L52 8.5L48 3.5L40 5L8 7.6Z"></path>
<path class="tabs-multitool__line" d="M8-5.8L42-3.6"></path>
<g clip-path="url(#tabs-multitool-clip-2)">
<rect class="tabs-multitool__glint" x="0" y="-12" width="10" height="24"></rect>
</g>
</svg>
</span>
<span class="tabs-multitool__tool" data-end="r" style="width:72px;--m:1;--d:0.22s">
<svg width="72" height="24" viewBox="-10 -12 72 24">
<defs>
<clipPath id="tabs-multitool-clip-3">
<path d="M-9 0A9 9 0 0 1 0-9H26L30-4H36L40-8H56L60-5V5L56 8L0 9A9 9 0 0 1-9 0Z"></path>
</clipPath>
</defs>
<path class="tabs-multitool__steel" d="M-9 0A9 9 0 0 1 0-9H26L30-4H36L40-8H56L60-5V5L56 8L0 9A9 9 0 0 1-9 0Z"></path>
<path class="tabs-multitool__edge" d="M54-7.6L60-5V5L54 7.8Z"></path>
<path class="tabs-multitool__line" d="M8-6.4H24"></path>
<g clip-path="url(#tabs-multitool-clip-3)">
<rect class="tabs-multitool__glint" x="0" y="-12" width="10" height="24"></rect>
</g>
</svg>
</span>
<span class="tabs-multitool__scale">
<span class="tabs-multitool__inlay">No.4</span>
</span>
</span>
</div>
<div class="tabs-multitool__tabs" role="tablist" aria-label="Tools">
<button class="tabs-multitool__tab" type="button" role="tab" id="tabs-multitool-tab-0" aria-selected="true" aria-controls="tabs-multitool-panel-0" tabindex="0">Blade</button>
<button class="tabs-multitool__tab" type="button" role="tab" id="tabs-multitool-tab-1" aria-selected="false" aria-controls="tabs-multitool-panel-1" tabindex="-1">Saw</button>
<button class="tabs-multitool__tab" type="button" role="tab" id="tabs-multitool-tab-2" aria-selected="false" aria-controls="tabs-multitool-panel-2" tabindex="-1">Opener</button>
<button class="tabs-multitool__tab" type="button" role="tab" id="tabs-multitool-tab-3" aria-selected="false" aria-controls="tabs-multitool-panel-3" tabindex="-1">Driver</button>
</div>
<div class="tabs-multitool__panel" role="tabpanel" id="tabs-multitool-panel-0" aria-labelledby="tabs-multitool-tab-0">A 7 cm blade that locks open, for parcels, rope and apples.</div>
<div class="tabs-multitool__panel" role="tabpanel" id="tabs-multitool-panel-1" aria-labelledby="tabs-multitool-tab-1" hidden="">Cuts green wood as thick as your thumb, on the pull stroke.</div>
<div class="tabs-multitool__panel" role="tabpanel" id="tabs-multitool-panel-2" aria-labelledby="tabs-multitool-tab-2" hidden="">Opens a tin in a few turns, and the notch strips wire.</div>
<div class="tabs-multitool__panel" role="tabpanel" id="tabs-multitool-panel-3" aria-labelledby="tabs-multitool-tab-3" hidden="">A flat 3 mm tip for screws, and a lip that lifts bottle caps.</div>
</div>.tabs-multitool { --shade: light-dark(#373a40, #121417); --steel: light-dark(#6d7179, #a7abb2); width: 230px; }
.tabs-multitool__bench { position: relative; height: 84px; }
.tabs-multitool__body { position: absolute; left: 0; top: 51px; width: 112px; height: 26px; transition: transform 0.42s cubic-bezier(0.65, 0, 0.35, 1) 0.26s; }
/* A tool swings about its pivot. Closing is a quick snap shut; opening waits for the
last tool and the body, then flicks out past straight and settles on the lock. */
.tabs-multitool__tool { position: absolute; top: 1px; height: 24px; transform-origin: 10px 12px; transition: transform 0.3s cubic-bezier(0.55, 0, 0.8, 0.3); }
.tabs-multitool__tool[data-end='l'] { left: 3px; transform: rotate(180deg) scaleX(-1); }
.tabs-multitool__tool[data-end='r'] { left: 89px; transform: rotate(-180deg); }
.tabs-multitool__tool[data-open] { transform: rotate(0deg) scaleX(var(--m)); transition: transform 0.55s cubic-bezier(0.25, 1.5, 0.45, 1) var(--d, 0s); }
.tabs-multitool__tool svg { display: block; overflow: visible; }
.tabs-multitool__steel { fill: var(--steel); }
.tabs-multitool__edge { fill: color-mix(in oklab, var(--steel) 55%, light-dark(#373a40, #f1f2f3)); }
.tabs-multitool__line { fill: none; stroke: color-mix(in oklab, var(--steel) 60%, var(--shade)); stroke-width: 1.4; stroke-linecap: round; }
.tabs-multitool__glint { fill: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent); transform: translateX(-30px) skewX(-24deg); }
.tabs-multitool__tool[data-open] .tabs-multitool__glint { animation: tabs-multitool-glint 0.7s ease-in-out calc(var(--d, 0s) + 0.45s) both; }
/* The scale covers the folded tools, with a rivet at each pivot and a small inlay. */
.tabs-multitool__scale { position: absolute; inset: 0; border-radius: 13px; background: radial-gradient(circle at 13px 13px, var(--steel) 0 3px, color-mix(in oklab, var(--steel) 50%, var(--shade)) 3.6px, transparent 4.2px), radial-gradient(circle at 99px 13px, var(--steel) 0 3px, color-mix(in oklab, var(--steel) 50%, var(--shade)) 3.6px, transparent 4.2px), linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035) 55%, light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), inset 0 -1.5px 0 color-mix(in oklab, var(--shade) 40%, transparent), 0 8px 12px -6px light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.6)); }
.tabs-multitool__inlay { position: absolute; left: 40px; top: 8px; width: 32px; height: 10px; border-radius: 3px; background: linear-gradient(var(--steel), color-mix(in oklab, var(--steel) 70%, var(--shade))); color: light-dark(#ebedf0, #232529); font: 700 6.5px/10px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.12em; text-align: center; }
.tabs-multitool__tabs { display: flex; justify-content: space-between; margin-top: 10px; }
.tabs-multitool__tab { position: relative; padding: 6px 4px 7px; border: 0; background: none; color: light-dark(#5f626a, #a7abb2); font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; transition: color 0.2s; }
.tabs-multitool__tab::before { content: ''; position: absolute; left: 50%; top: 0; width: 1px; height: 3px; background: currentColor; transition: height 0.25s; }
.tabs-multitool__tab:hover { color: light-dark(#42454c, #e5e6e9); }
.tabs-multitool__tab[aria-selected='true'] { color: light-dark(#373a40, #f1f2f3); }
.tabs-multitool__tab[aria-selected='true']::before { height: 5px; width: 3px; translate: -1px 0; border-radius: 1px; }
.tabs-multitool__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; border-radius: 4px; }
.tabs-multitool__panel { min-height: 3em; margin-top: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; line-height: 1.5; }
@keyframes tabs-multitool-glint { from { transform: translateX(-30px) skewX(-24deg); } to { transform: translateX(110px) skewX(-24deg); } }
@media (prefers-reduced-motion: reduce) {
.tabs-multitool__tool, .tabs-multitool__tool[data-open], .tabs-multitool__body { transition: none; }
.tabs-multitool__tool[data-open] .tabs-multitool__glint { animation: none; }
}function initTabsMultitool(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-multitool__tool');
var body = root.querySelector('.tabs-multitool__body');
var BODY_X = { l: 96, r: 36 }; // where the body sits so the open tool and body are centred
function update(from) {
var end = tools[sel].getAttribute('data-end');
// Open once the last tool is shut, and once the body has slid over if it had to.
var delay = tools[from].getAttribute('data-end') === end ? '0.22s' : '0.62s';
tools.forEach(function (tool, k) {
tool.style.setProperty('--d', delay);
tool.toggleAttribute('data-open', k === sel);
});
body.style.transform = 'translateX(' + BODY_X[end] + 'px)';
}
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);
};
}
document.querySelectorAll('.tabs-multitool').forEach(initTabsMultitool);Goal: Create a "Multitool" tab.
Visual behavior: A pocket multitool: pick a tool and the last one folds shut, the body slides over if it has to, and the new one swings out on its pivot and locks open with a glint.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each tool is drawn pointing away from its pivot and sits under the scale. Tools at the left end are mirrored, so all four share one rotation: 180 degrees shut, 0 open.
- Closing is a quick ease-in. Opening waits for the last tool (and for the body, if the new tool lives at the other end) and uses a curve that passes straight, so it flicks past and settles on the lock.
- The body slides so the open tool and the body stay centred. A skewed highlight, clipped to the tool, sweeps along the steel once it has locked.
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 tools switch without swinging or a glint.
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 "Multitool" tab.
Visual behavior: A pocket multitool: pick a tool and the last one folds shut, the body slides over if it has to, and the new one swings out on its pivot and locks open with a glint.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each tool is drawn pointing away from its pivot and sits under the scale. Tools at the left end are mirrored, so all four share one rotation: 180 degrees shut, 0 open.
- Closing is a quick ease-in. Opening waits for the last tool (and for the body, if the new tool lives at the other end) and uses a curve that passes straight, so it flicks past and settles on the lock.
- The body slides so the open tool and the body stay centred. A skewed highlight, clipped to the tool, sweeps along the steel once it has locked.
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 tools switch without swinging or a glint.
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 pocket multitool: pick a tool and the last one folds shut, the body slides over if it has to, and the new one swings out on its pivot and locks open with a glint.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, spring
- Techniques
- css transition, keyframes, svg, clip path
- 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 tools switch without swinging or a glint.