Multitool
import { useId } from 'react';
// Every tool is drawn pointing right from its pivot at (0, 10). Right-hand
// tools fold back to 180deg; left-hand ones fold to 0 and open the other way.
const TOOLS = [
{ value: 'blade', label: 'Blade', side: 'r', open: 360, shift: -40, d: 'M0 4.5H64C78 4.5 90 7.5 99 12.5C84 15.5 70 15.5 54 15.5H0Z', extra: 'M20 7.4A2.4 2.4 0 0 1 24.6 7.4' },
{ value: 'saw', label: 'Saw', side: 'r', open: 360, shift: -40, d: 'M0 5H88L92 9V12H86L83 15L80 12L77 15L74 12L71 15L68 12L65 15L62 12L59 15L56 12L53 15L50 12L47 15L44 12L41 15L38 12L35 15L32 12L29 15L26 12L23 15L20 12H0Z', extra: '' },
{ value: 'opener', label: 'Opener', side: 'l', open: -180, shift: 40, d: 'M0 6H62L66 4H76V8.4H80L84 7.8V12.2L80 11.6H76V16H66L62 14H0Z', extra: 'M68 8.2H73V11.8H68' },
{ value: 'corkscrew', label: 'Corkscrew', side: 'l', open: -90, shift: 0, d: 'M0 8.5H14V11.5H0Z', extra: 'M14 10C18 3 22 3 24 10S30 17 34 10S40 3 44 10S50 17 54 10S60 3 64 10S70 17 74 10L82 10' },
];
const at = (n: number) => `.radio-multitool:has(.radio-multitool__label:nth-child(${n + 1}) input:checked)`;
const SNAP = 'linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1)';
const css = `
.radio-multitool { --scale: light-dark(#42454c, #232529); --scale-hi: light-dark(#5f626a, #393c41); --steel-hi: light-dark(#fbfcfd, #e5e6e9); --steel-lo: light-dark(#6d7179, #7d8087); display: grid; justify-items: center; gap: 16px; }
.radio-multitool__bench { position: relative; width: 262px; height: 136px; }
.radio-multitool__knife { position: absolute; left: 56px; top: 92px; width: 150px; height: 38px; transform: translateX(var(--shift, 0px)); transition: transform 0.7s cubic-bezier(0.5, 0, 0.2, 1); }
.radio-multitool__body { position: absolute; inset: 0; z-index: 1; border-radius: 19px; background: radial-gradient(120% 90% at 50% 0%, var(--scale-hi), var(--scale) 60%); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 18%, transparent), inset 0 -2px 0 light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)), 0 0 0 1.5px color-mix(in oklab, var(--steel-lo) 70%, transparent), 0 10px 14px -6px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.radio-multitool__rivet { position: absolute; top: 14px; width: 10px; height: 10px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, var(--steel-hi), var(--steel-lo) 75%); box-shadow: 0 0 0 1px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
.radio-multitool__mark { position: absolute; left: 50%; top: 50%; width: 30px; height: 2px; margin: -1px 0 0 -15px; border-radius: 1px; background: color-mix(in oklab, var(--scale-hi) 60%, transparent); box-shadow: 0 -5px 0 color-mix(in oklab, var(--scale-hi) 35%, transparent), 0 5px 0 color-mix(in oklab, var(--scale-hi) 35%, transparent); }
/* A tool sits behind the scales with its pivot on a rivet, folded away until picked. */
.radio-multitool__tool { position: absolute; top: 9px; width: 100px; height: 20px; overflow: visible; transform-origin: 0 50%; transition: transform 0.42s cubic-bezier(0.55, 0, 0.45, 1); }
.radio-multitool__tool--r { left: 134px; transform: rotate(180deg); }
.radio-multitool__tool--l { left: 16px; transform: rotate(0deg); }
.radio-multitool__tool path { fill: url(#radio-multitool-steel); stroke: color-mix(in oklab, var(--steel-lo) 70%, light-dark(#fbfcfd, #121417)); stroke-width: 0.6; }
.radio-multitool__tool path + path { fill: none; stroke: var(--steel-hi); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.radio-multitool__tool[data-tool='corkscrew'] path + path { stroke: url(#radio-multitool-steel); stroke-width: 2.6; }
.radio-multitool__stop-hi { stop-color: var(--steel-hi); }
.radio-multitool__stop-lo { stop-color: var(--steel-lo); }
${TOOLS.map((t, i) => `${at(i)} { --shift: ${t.shift}px; }\n ${at(i)} .radio-multitool__tool[data-tool='${t.value}'] { transform: rotate(${t.open}deg); transition: transform 0.8s ${SNAP} 0.12s; }\n ${at(i)} .radio-multitool__knife { animation: radio-multitool-click${i} 0.3s ease-out 0.5s; }\n @keyframes radio-multitool-click${i} { 30% { rotate: ${t.side === 'r' ? -1.2 : 1.2}deg; } }`).join('\n ')}
.radio-multitool__labels { display: flex; gap: 18px; }
.radio-multitool__label { position: relative; color: light-dark(#6d7179, #7d8087); font: 700 10px/1 system-ui, sans-serif; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; transition: color 0.25s; }
.radio-multitool__label::before { content: ''; position: absolute; left: 50%; top: -9px; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); transform: scale(0); transition: transform 0.3s; }
.radio-multitool__label:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-multitool__label:has(input:checked) { color: light-dark(#373a40, #f1f2f3); }
.radio-multitool__label:has(input:checked)::before { transform: scale(1); transition: transform 0.45s ${SNAP} 0.3s; }
.radio-multitool__label:has(input:focus-visible) { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 2px; }
.radio-multitool__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-multitool *, .radio-multitool *::before { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const name = useId();
return (
<>
<style href="kit-radios-multitool" precedence="kit">
{css}
</style>
<div className="radio-multitool">
<div className="radio-multitool__bench" aria-hidden="true">
<svg width="0" height="0" style={{ position: 'absolute' }}>
<linearGradient id="radio-multitool-steel" x1="0" y1="0" x2="0" y2="1">
<stop className="radio-multitool__stop-hi" offset="0" />
<stop className="radio-multitool__stop-lo" offset="1" />
</linearGradient>
</svg>
<div className="radio-multitool__knife">
{TOOLS.map((t) => (
<svg key={t.value} className={`radio-multitool__tool radio-multitool__tool--${t.side}`} data-tool={t.value} viewBox="0 0 100 20">
<path d={t.d} />
{t.extra && <path d={t.extra} />}
</svg>
))}
<span className="radio-multitool__body">
<span className="radio-multitool__rivet" style={{ left: 11 }} />
<span className="radio-multitool__rivet" style={{ left: 129 }} />
<span className="radio-multitool__mark" />
</span>
</div>
</div>
<div className="radio-multitool__labels" role="radiogroup" aria-label="Tool">
{TOOLS.map((t, i) => (
<label key={t.value} className="radio-multitool__label">
<input className="radio-multitool__input" type="radio" name={name} value={t.value} defaultChecked={i === 0} />
{t.label}
</label>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<div class="radio-multitool">
<div class="radio-multitool__bench" aria-hidden="true">
<svg width="0" height="0" style="position:absolute">
<linearGradient id="radio-multitool-steel" x1="0" y1="0" x2="0" y2="1">
<stop class="radio-multitool__stop-hi" offset="0"></stop>
<stop class="radio-multitool__stop-lo" offset="1"></stop>
</lineargradient>
</svg>
<div class="radio-multitool__knife">
<svg class="radio-multitool__tool radio-multitool__tool--r" data-tool="blade" viewBox="0 0 100 20">
<path d="M0 4.5H64C78 4.5 90 7.5 99 12.5C84 15.5 70 15.5 54 15.5H0Z"></path>
<path d="M20 7.4A2.4 2.4 0 0 1 24.6 7.4"></path>
</svg>
<svg class="radio-multitool__tool radio-multitool__tool--r" data-tool="saw" viewBox="0 0 100 20">
<path d="M0 5H88L92 9V12H86L83 15L80 12L77 15L74 12L71 15L68 12L65 15L62 12L59 15L56 12L53 15L50 12L47 15L44 12L41 15L38 12L35 15L32 12L29 15L26 12L23 15L20 12H0Z"></path>
</svg>
<svg class="radio-multitool__tool radio-multitool__tool--l" data-tool="opener" viewBox="0 0 100 20">
<path d="M0 6H62L66 4H76V8.4H80L84 7.8V12.2L80 11.6H76V16H66L62 14H0Z"></path>
<path d="M68 8.2H73V11.8H68"></path>
</svg>
<svg class="radio-multitool__tool radio-multitool__tool--l" data-tool="corkscrew" viewBox="0 0 100 20">
<path d="M0 8.5H14V11.5H0Z"></path>
<path d="M14 10C18 3 22 3 24 10S30 17 34 10S40 3 44 10S50 17 54 10S60 3 64 10S70 17 74 10L82 10"></path>
</svg>
<span class="radio-multitool__body">
<span class="radio-multitool__rivet" style="left:11px"></span>
<span class="radio-multitool__rivet" style="left:129px"></span>
<span class="radio-multitool__mark"></span>
</span>
</div>
</div>
<div class="radio-multitool__labels" role="radiogroup" aria-label="Tool">
<label class="radio-multitool__label">
<input class="radio-multitool__input" type="radio" name="radio-multitool" checked="" value="blade"/>
Blade
</label>
<label class="radio-multitool__label">
<input class="radio-multitool__input" type="radio" name="radio-multitool" value="saw"/>
Saw
</label>
<label class="radio-multitool__label">
<input class="radio-multitool__input" type="radio" name="radio-multitool" value="opener"/>
Opener
</label>
<label class="radio-multitool__label">
<input class="radio-multitool__input" type="radio" name="radio-multitool" value="corkscrew"/>
Corkscrew
</label>
</div>
</div>.radio-multitool { --scale: light-dark(#42454c, #232529); --scale-hi: light-dark(#5f626a, #393c41); --steel-hi: light-dark(#fbfcfd, #e5e6e9); --steel-lo: light-dark(#6d7179, #7d8087); display: grid; justify-items: center; gap: 16px; }
.radio-multitool__bench { position: relative; width: 262px; height: 136px; }
.radio-multitool__knife { position: absolute; left: 56px; top: 92px; width: 150px; height: 38px; transform: translateX(var(--shift, 0px)); transition: transform 0.7s cubic-bezier(0.5, 0, 0.2, 1); }
.radio-multitool__body { position: absolute; inset: 0; z-index: 1; border-radius: 19px; background: radial-gradient(120% 90% at 50% 0%, var(--scale-hi), var(--scale) 60%); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 18%, transparent), inset 0 -2px 0 light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)), 0 0 0 1.5px color-mix(in oklab, var(--steel-lo) 70%, transparent), 0 10px 14px -6px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.radio-multitool__rivet { position: absolute; top: 14px; width: 10px; height: 10px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, var(--steel-hi), var(--steel-lo) 75%); box-shadow: 0 0 0 1px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
.radio-multitool__mark { position: absolute; left: 50%; top: 50%; width: 30px; height: 2px; margin: -1px 0 0 -15px; border-radius: 1px; background: color-mix(in oklab, var(--scale-hi) 60%, transparent); box-shadow: 0 -5px 0 color-mix(in oklab, var(--scale-hi) 35%, transparent), 0 5px 0 color-mix(in oklab, var(--scale-hi) 35%, transparent); }
/* A tool sits behind the scales with its pivot on a rivet, folded away until picked. */
.radio-multitool__tool { position: absolute; top: 9px; width: 100px; height: 20px; overflow: visible; transform-origin: 0 50%; transition: transform 0.42s cubic-bezier(0.55, 0, 0.45, 1); }
.radio-multitool__tool--r { left: 134px; transform: rotate(180deg); }
.radio-multitool__tool--l { left: 16px; transform: rotate(0deg); }
.radio-multitool__tool path { fill: url(#radio-multitool-steel); stroke: color-mix(in oklab, var(--steel-lo) 70%, light-dark(#fbfcfd, #121417)); stroke-width: 0.6; }
.radio-multitool__tool path + path { fill: none; stroke: var(--steel-hi); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.radio-multitool__tool[data-tool='corkscrew'] path + path { stroke: url(#radio-multitool-steel); stroke-width: 2.6; }
.radio-multitool__stop-hi { stop-color: var(--steel-hi); }
.radio-multitool__stop-lo { stop-color: var(--steel-lo); }
.radio-multitool:has(.radio-multitool__label:nth-child(1) input:checked) { --shift: -40px; }
.radio-multitool:has(.radio-multitool__label:nth-child(1) input:checked) .radio-multitool__tool[data-tool='blade'] { transform: rotate(360deg); transition: transform 0.8s linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1) 0.12s; }
.radio-multitool:has(.radio-multitool__label:nth-child(1) input:checked) .radio-multitool__knife { animation: radio-multitool-click0 0.3s ease-out 0.5s; }
@keyframes radio-multitool-click0 { 30% { rotate: -1.2deg; } }
.radio-multitool:has(.radio-multitool__label:nth-child(2) input:checked) { --shift: -40px; }
.radio-multitool:has(.radio-multitool__label:nth-child(2) input:checked) .radio-multitool__tool[data-tool='saw'] { transform: rotate(360deg); transition: transform 0.8s linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1) 0.12s; }
.radio-multitool:has(.radio-multitool__label:nth-child(2) input:checked) .radio-multitool__knife { animation: radio-multitool-click1 0.3s ease-out 0.5s; }
@keyframes radio-multitool-click1 { 30% { rotate: -1.2deg; } }
.radio-multitool:has(.radio-multitool__label:nth-child(3) input:checked) { --shift: 40px; }
.radio-multitool:has(.radio-multitool__label:nth-child(3) input:checked) .radio-multitool__tool[data-tool='opener'] { transform: rotate(-180deg); transition: transform 0.8s linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1) 0.12s; }
.radio-multitool:has(.radio-multitool__label:nth-child(3) input:checked) .radio-multitool__knife { animation: radio-multitool-click2 0.3s ease-out 0.5s; }
@keyframes radio-multitool-click2 { 30% { rotate: 1.2deg; } }
.radio-multitool:has(.radio-multitool__label:nth-child(4) input:checked) { --shift: 0px; }
.radio-multitool:has(.radio-multitool__label:nth-child(4) input:checked) .radio-multitool__tool[data-tool='corkscrew'] { transform: rotate(-90deg); transition: transform 0.8s linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1) 0.12s; }
.radio-multitool:has(.radio-multitool__label:nth-child(4) input:checked) .radio-multitool__knife { animation: radio-multitool-click3 0.3s ease-out 0.5s; }
@keyframes radio-multitool-click3 { 30% { rotate: 1.2deg; } }
.radio-multitool__labels { display: flex; gap: 18px; }
.radio-multitool__label { position: relative; color: light-dark(#6d7179, #7d8087); font: 700 10px/1 system-ui, sans-serif; letter-spacing: 0.14em; text-transform: uppercase; cursor: pointer; transition: color 0.25s; }
.radio-multitool__label::before { content: ''; position: absolute; left: 50%; top: -9px; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); transform: scale(0); transition: transform 0.3s; }
.radio-multitool__label:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-multitool__label:has(input:checked) { color: light-dark(#373a40, #f1f2f3); }
.radio-multitool__label:has(input:checked)::before { transform: scale(1); transition: transform 0.45s linear(0, 0.156, 0.492, 0.838, 1.091, 1.215, 1.228, 1.169, 1.086, 1.012, 0.964, 0.946, 0.951, 0.968, 0.987, 1.002, 1.011, 1.013, 1.01, 1.006, 1.001, 0.998, 0.997, 0.997, 1) 0.3s; }
.radio-multitool__label:has(input:focus-visible) { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 2px; }
.radio-multitool__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-multitool *, .radio-multitool *::before { animation: none !important; transition: none !important; }
}Goal: Create a "Multitool" radio.
Visual behavior: A pocket knife: the tool you pick swings out over the top on its pivot and clicks open, the last one folds away, and the knife slides over to keep the open tool in the middle.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Each tool is an SVG drawn pointing out from its pivot and sits behind the scales. Right-hand tools fold to 180deg and open to 360deg, so they swing over the top instead of through the handle; left-hand ones turn the other way, and the corkscrew stops upright at -90deg.
- Opening uses a linear() spring for the detent snap and folding a plain ease. :has() also shifts the whole knife so it stays balanced, and a keyframe per tool jolts it as the tool clicks home.
Accessibility: A role="radiogroup" labelled "Tool" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The tool names are the visible labels and focus outlines the name. The knife drawing is aria-hidden. Under prefers-reduced-motion the tools open and fold 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 "Multitool" radio.
Visual behavior: A pocket knife: the tool you pick swings out over the top on its pivot and clicks open, the last one folds away, and the knife slides over to keep the open tool in the middle.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Each tool is an SVG drawn pointing out from its pivot and sits behind the scales. Right-hand tools fold to 180deg and open to 360deg, so they swing over the top instead of through the handle; left-hand ones turn the other way, and the corkscrew stops upright at -90deg.
- Opening uses a linear() spring for the detent snap and folding a plain ease. :has() also shifts the whole knife so it stays balanced, and a keyframe per tool jolts it as the tool clicks home.
Accessibility: A role="radiogroup" labelled "Tool" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The tool names are the visible labels and focus outlines the name. The knife drawing is aria-hidden. Under prefers-reduced-motion the tools open and fold 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 pocket knife: the tool you pick swings out over the top on its pivot and clicks open, the last one folds away, and the knife slides over to keep the open tool in the middle.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, spring, keyframe
- Techniques
- transform only, css transition, css variables, spring, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" labelled "Tool" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The tool names are the visible labels and focus outlines the name. The knife drawing is aria-hidden. Under prefers-reduced-motion the tools open and fold at once.