6 min, runny yolk8 min, set at the edge10 min, firm all through
Units
React + Tailwind
import { useId } from 'react';
const BLADE = 176;
const CM = 14; // px per centimetre on the drawing
const IN = CM * 2.54;
const at = (v: string) => `.radio-units:has(input[value='${v}']:checked)`;
const css = `
@property --radio-units-x { syntax: '<length>'; inherits: true; initial-value: ${BLADE + 30}px; }
.radio-units { --blade: light-dark(#f3f4f7, #e5e6e9); --tick: light-dark(#373a40, #121417); --shade: light-dark(#373a40, #121417); --case: light-dark(#42454c, #232529); --steel: light-dark(#6d7179, #a7abb2); display: grid; justify-items: center; gap: 18px; }
.radio-units__tool { position: relative; width: 262px; height: 112px; }
/* The case: a rubber-cornered body, hub screw, lock and the size printed on it. */
.radio-units__case { position: absolute; left: 2px; top: 10px; width: 88px; height: 88px; border-radius: 22px 22px 22px 8px; background: radial-gradient(110% 100% at 30% 15%, color-mix(in oklab, var(--case) 75%, light-dark(#373a40, #f1f2f3)), var(--case) 60%); box-shadow: inset 0 0 0 5px color-mix(in oklab, var(--case) 70%, light-dark(#fbfcfd, #121417)), inset 0 1px 0 6px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent), 0 12px 18px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); z-index: 2; }
.radio-units__hub { position: absolute; left: 50%; top: 42%; width: 32px; height: 32px; translate: -50% -50%; border-radius: 50%; background: radial-gradient(circle at 40% 35%, color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)), var(--steel) 70%); box-shadow: 0 0 0 3px color-mix(in oklab, var(--case) 80%, light-dark(#fbfcfd, #121417)); }
.radio-units__hub::after { content: ''; position: absolute; left: 50%; top: 50%; width: 10px; height: 2px; translate: -50% -50%; rotate: 35deg; background: color-mix(in oklab, var(--steel) 50%, light-dark(#fbfcfd, #121417)); }
.radio-units__lock { position: absolute; right: 12px; top: -4px; width: 22px; height: 8px; border-radius: 3px; background: var(--steel); }
.radio-units__size { position: absolute; left: 0; right: 0; bottom: 11px; display: grid; color: light-dark(#dbdde1, #c6c8cd); font: 700 9px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.08em; text-align: center; }
.radio-units__size > span { grid-area: 1 / 1; transition: opacity 0.3s 0.3s; }
${at('cm')} .radio-units__size > :last-child, ${at('in')} .radio-units__size > :first-child { opacity: 0; transition-delay: 0s; }
/* The blade: two printed sides. A pinch rides along it where it turns over. */
.radio-units__blade { position: absolute; left: 84px; top: 70px; width: ${BLADE}px; height: 20px; filter: drop-shadow(0 4px 3px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-units__strip { position: absolute; inset: 0; mask: radial-gradient(22px 9.6px at var(--radio-units-x) 0, transparent 98%, #000) , radial-gradient(22px 9.6px at var(--radio-units-x) 100%, transparent 98%, #000); mask-composite: intersect; }
.radio-units__face { position: absolute; inset: 0; background-color: var(--blade); box-shadow: inset 0 2px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent), inset 0 -1.5px 0 color-mix(in oklab, var(--shade) 25%, transparent); }
.radio-units__face--cm { background-image: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px ${CM}px), repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px ${CM / 2}px), repeating-linear-gradient(90deg, color-mix(in oklab, var(--tick) 60%, transparent) 0 0.7px, transparent 0.7px ${CM / 10}px); background-size: 100% 9px, 100% 5px, 100% 3px; background-repeat: no-repeat; }
.radio-units__face--in { background-image: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px ${IN}px), repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px ${IN / 2}px), repeating-linear-gradient(90deg, var(--tick) 0 0.8px, transparent 0.8px ${IN / 4}px), repeating-linear-gradient(90deg, color-mix(in oklab, var(--tick) 60%, transparent) 0 0.7px, transparent 0.7px ${IN / 8}px); background-size: 100% 11px, 100% 7px, 100% 5px, 100% 3px; background-repeat: no-repeat; clip-path: inset(0 100% 0 0); }
.radio-units__face span { position: absolute; top: 9px; color: var(--tick); font: 800 7.5px/1 system-ui, sans-serif; }
.radio-units__face--in span { top: 10px; font-size: 8.5px; }
.radio-units__face b { position: absolute; left: 4px; bottom: 2px; color: color-mix(in oklab, var(--tick) 55%, transparent); font: 700 5.5px/1 system-ui, sans-serif; letter-spacing: 0.1em; }
.radio-units__turn { position: absolute; inset: 0; background: linear-gradient(90deg, transparent calc(var(--radio-units-x) - 26px), color-mix(in oklab, var(--shade) 45%, transparent) var(--radio-units-x), transparent calc(var(--radio-units-x) + 26px)); pointer-events: none; }
${at('in')} { animation: radio-units-over 0.95s cubic-bezier(0.45, 0, 0.35, 1) forwards; }
${at('cm')} { animation: radio-units-back 0.95s cubic-bezier(0.45, 0, 0.35, 1) forwards; }
${at('in')} .radio-units__face--in { clip-path: inset(0 calc(100% - var(--radio-units-x)) 0 0); }
${at('cm')} .radio-units__face--in { clip-path: inset(0 0 0 var(--radio-units-x)); }
@keyframes radio-units-over { from { --radio-units-x: -26px; } to { --radio-units-x: ${BLADE + 30}px; } }
@keyframes radio-units-back { from { --radio-units-x: -26px; } to { --radio-units-x: ${BLADE + 30}px; } }
.radio-units__hook { position: absolute; right: -5px; top: -4px; width: 6px; height: 28px; border-radius: 1px 2px 2px 1px; background: linear-gradient(90deg, var(--steel), color-mix(in oklab, var(--steel) 70%, light-dark(#373a40, #f1f2f3))); box-shadow: 0 2px 2px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); }
${at('in')} .radio-units__hook { animation: radio-units-flick-a 0.4s ease-out 0.8s; }
${at('cm')} .radio-units__hook { animation: radio-units-flick-b 0.4s ease-out 0.8s; }
@keyframes radio-units-flick-a { 40% { transform: scaleY(0.2); } }
@keyframes radio-units-flick-b { 40% { transform: scaleY(0.2); } }
.radio-units__opts { display: flex; gap: 22px; }
.radio-units__opt { position: relative; display: flex; align-items: center; gap: 7px; color: light-dark(#6d7179, #7d8087); font: 600 12.5px system-ui, sans-serif; cursor: pointer; transition: color 0.25s; }
.radio-units__opt b { font: 700 11px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; padding: 2px 5px; border-radius: 4px; box-shadow: inset 0 0 0 1px currentColor; }
.radio-units__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-units__opt:has(input:checked) { color: light-dark(#373a40, #f1f2f3); }
.radio-units__opt:has(input:focus-visible) { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 4px; }
.radio-units__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-units, .radio-units * { animation: none !important; transition: none !important; }
${at('in')} .radio-units__face--in { clip-path: none; }
}
`;
export function Preview() {
const name = useId();
return (
<>
<style href="kit-radios-units" precedence="kit">
{css}
</style>
<div className="radio-units">
<div className="radio-units__tool" aria-hidden="true">
<span className="radio-units__blade">
<span className="radio-units__strip">
<span className="radio-units__face radio-units__face--cm">
{Array.from({ length: 12 }, (_, k) => (
<span key={k} style={{ left: (k + 1) * CM + 1.5 }}>
{k + 1}
</span>
))}
<b>CM</b>
</span>
<span className="radio-units__face radio-units__face--in">
{Array.from({ length: 4 }, (_, k) => (
<span key={k} style={{ left: (k + 1) * IN + 1.5 }}>
{k + 1}
</span>
))}
<b>INCH</b>
</span>
<span className="radio-units__turn" />
</span>
<span className="radio-units__hook" />
</span>
<span className="radio-units__case">
<span className="radio-units__lock" />
<span className="radio-units__hub" />
<span className="radio-units__size">
<span>5 m</span>
<span>16 ft</span>
</span>
</span>
</div>
<div className="radio-units__opts" role="radiogroup" aria-label="Units">
<label className="radio-units__opt">
<input className="radio-units__input" type="radio" name={name} value="cm" defaultChecked />
<b aria-hidden="true">cm</b>
Metric
</label>
<label className="radio-units__opt">
<input className="radio-units__input" type="radio" name={name} value="in" />
<b aria-hidden="true">in</b>
Imperial
</label>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="radio-units">
<div class="radio-units__tool" aria-hidden="true">
<span class="radio-units__blade">
<span class="radio-units__strip">
<span class="radio-units__face radio-units__face--cm">
<span style="left:15.5px">1</span>
<span style="left:29.5px">2</span>
<span style="left:43.5px">3</span>
<span style="left:57.5px">4</span>
<span style="left:71.5px">5</span>
<span style="left:85.5px">6</span>
<span style="left:99.5px">7</span>
<span style="left:113.5px">8</span>
<span style="left:127.5px">9</span>
<span style="left:141.5px">10</span>
<span style="left:155.5px">11</span>
<span style="left:169.5px">12</span>
<b>CM</b>
</span>
<span class="radio-units__face radio-units__face--in">
<span style="left:37.06px">1</span>
<span style="left:72.62px">2</span>
<span style="left:108.18px">3</span>
<span style="left:143.74px">4</span>
<b>INCH</b>
</span>
<span class="radio-units__turn"></span>
</span>
<span class="radio-units__hook"></span>
</span>
<span class="radio-units__case">
<span class="radio-units__lock"></span>
<span class="radio-units__hub"></span>
<span class="radio-units__size">
<span>5 m</span>
<span>16 ft</span>
</span>
</span>
</div>
<div class="radio-units__opts" role="radiogroup" aria-label="Units">
<label class="radio-units__opt">
<input class="radio-units__input" type="radio" name="radio-units" checked="" value="cm"/>
<b aria-hidden="true">cm</b>
Metric
</label>
<label class="radio-units__opt">
<input class="radio-units__input" type="radio" name="radio-units" value="in"/>
<b aria-hidden="true">in</b>
Imperial
</label>
</div>
</div>CSS
@property --radio-units-x { syntax: '<length>'; inherits: true; initial-value: 206px; }
.radio-units { --blade: light-dark(#f3f4f7, #e5e6e9); --tick: light-dark(#373a40, #121417); --shade: light-dark(#373a40, #121417); --case: light-dark(#42454c, #232529); --steel: light-dark(#6d7179, #a7abb2); display: grid; justify-items: center; gap: 18px; }
.radio-units__tool { position: relative; width: 262px; height: 112px; }
/* The case: a rubber-cornered body, hub screw, lock and the size printed on it. */
.radio-units__case { position: absolute; left: 2px; top: 10px; width: 88px; height: 88px; border-radius: 22px 22px 22px 8px; background: radial-gradient(110% 100% at 30% 15%, color-mix(in oklab, var(--case) 75%, light-dark(#373a40, #f1f2f3)), var(--case) 60%); box-shadow: inset 0 0 0 5px color-mix(in oklab, var(--case) 70%, light-dark(#fbfcfd, #121417)), inset 0 1px 0 6px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent), 0 12px 18px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); z-index: 2; }
.radio-units__hub { position: absolute; left: 50%; top: 42%; width: 32px; height: 32px; translate: -50% -50%; border-radius: 50%; background: radial-gradient(circle at 40% 35%, color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)), var(--steel) 70%); box-shadow: 0 0 0 3px color-mix(in oklab, var(--case) 80%, light-dark(#fbfcfd, #121417)); }
.radio-units__hub::after { content: ''; position: absolute; left: 50%; top: 50%; width: 10px; height: 2px; translate: -50% -50%; rotate: 35deg; background: color-mix(in oklab, var(--steel) 50%, light-dark(#fbfcfd, #121417)); }
.radio-units__lock { position: absolute; right: 12px; top: -4px; width: 22px; height: 8px; border-radius: 3px; background: var(--steel); }
.radio-units__size { position: absolute; left: 0; right: 0; bottom: 11px; display: grid; color: light-dark(#dbdde1, #c6c8cd); font: 700 9px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.08em; text-align: center; }
.radio-units__size > span { grid-area: 1 / 1; transition: opacity 0.3s 0.3s; }
.radio-units:has(input[value='cm']:checked) .radio-units__size > :last-child, .radio-units:has(input[value='in']:checked) .radio-units__size > :first-child { opacity: 0; transition-delay: 0s; }
/* The blade: two printed sides. A pinch rides along it where it turns over. */
.radio-units__blade { position: absolute; left: 84px; top: 70px; width: 176px; height: 20px; filter: drop-shadow(0 4px 3px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-units__strip { position: absolute; inset: 0; mask: radial-gradient(22px 9.6px at var(--radio-units-x) 0, transparent 98%, #000) , radial-gradient(22px 9.6px at var(--radio-units-x) 100%, transparent 98%, #000); mask-composite: intersect; }
.radio-units__face { position: absolute; inset: 0; background-color: var(--blade); box-shadow: inset 0 2px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent), inset 0 -1.5px 0 color-mix(in oklab, var(--shade) 25%, transparent); }
.radio-units__face--cm { background-image: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px 14px), repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 7px), repeating-linear-gradient(90deg, color-mix(in oklab, var(--tick) 60%, transparent) 0 0.7px, transparent 0.7px 1.4px); background-size: 100% 9px, 100% 5px, 100% 3px; background-repeat: no-repeat; }
.radio-units__face--in { background-image: repeating-linear-gradient(90deg, var(--tick) 0 1.2px, transparent 1.2px 35.56px), repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 17.78px), repeating-linear-gradient(90deg, var(--tick) 0 0.8px, transparent 0.8px 8.89px), repeating-linear-gradient(90deg, color-mix(in oklab, var(--tick) 60%, transparent) 0 0.7px, transparent 0.7px 4.445px); background-size: 100% 11px, 100% 7px, 100% 5px, 100% 3px; background-repeat: no-repeat; clip-path: inset(0 100% 0 0); }
.radio-units__face span { position: absolute; top: 9px; color: var(--tick); font: 800 7.5px/1 system-ui, sans-serif; }
.radio-units__face--in span { top: 10px; font-size: 8.5px; }
.radio-units__face b { position: absolute; left: 4px; bottom: 2px; color: color-mix(in oklab, var(--tick) 55%, transparent); font: 700 5.5px/1 system-ui, sans-serif; letter-spacing: 0.1em; }
.radio-units__turn { position: absolute; inset: 0; background: linear-gradient(90deg, transparent calc(var(--radio-units-x) - 26px), color-mix(in oklab, var(--shade) 45%, transparent) var(--radio-units-x), transparent calc(var(--radio-units-x) + 26px)); pointer-events: none; }
.radio-units:has(input[value='in']:checked) { animation: radio-units-over 0.95s cubic-bezier(0.45, 0, 0.35, 1) forwards; }
.radio-units:has(input[value='cm']:checked) { animation: radio-units-back 0.95s cubic-bezier(0.45, 0, 0.35, 1) forwards; }
.radio-units:has(input[value='in']:checked) .radio-units__face--in { clip-path: inset(0 calc(100% - var(--radio-units-x)) 0 0); }
.radio-units:has(input[value='cm']:checked) .radio-units__face--in { clip-path: inset(0 0 0 var(--radio-units-x)); }
@keyframes radio-units-over { from { --radio-units-x: -26px; } to { --radio-units-x: 206px; } }
@keyframes radio-units-back { from { --radio-units-x: -26px; } to { --radio-units-x: 206px; } }
.radio-units__hook { position: absolute; right: -5px; top: -4px; width: 6px; height: 28px; border-radius: 1px 2px 2px 1px; background: linear-gradient(90deg, var(--steel), color-mix(in oklab, var(--steel) 70%, light-dark(#373a40, #f1f2f3))); box-shadow: 0 2px 2px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35)); }
.radio-units:has(input[value='in']:checked) .radio-units__hook { animation: radio-units-flick-a 0.4s ease-out 0.8s; }
.radio-units:has(input[value='cm']:checked) .radio-units__hook { animation: radio-units-flick-b 0.4s ease-out 0.8s; }
@keyframes radio-units-flick-a { 40% { transform: scaleY(0.2); } }
@keyframes radio-units-flick-b { 40% { transform: scaleY(0.2); } }
.radio-units__opts { display: flex; gap: 22px; }
.radio-units__opt { position: relative; display: flex; align-items: center; gap: 7px; color: light-dark(#6d7179, #7d8087); font: 600 12.5px system-ui, sans-serif; cursor: pointer; transition: color 0.25s; }
.radio-units__opt b { font: 700 11px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; padding: 2px 5px; border-radius: 4px; box-shadow: inset 0 0 0 1px currentColor; }
.radio-units__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-units__opt:has(input:checked) { color: light-dark(#373a40, #f1f2f3); }
.radio-units__opt:has(input:focus-visible) { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 4px; border-radius: 4px; }
.radio-units__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-units, .radio-units * { animation: none !important; transition: none !important; }
.radio-units:has(input[value='in']:checked) .radio-units__face--in { clip-path: none; }
}Goal: Create a "Units" radio.
Visual behavior: Metric or imperial on a tape measure: the blade twists over from the case to the hook to show its other side, with a pinch running along it where it turns.
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.
- One registered custom property (@property, <length>) is the position of the twist. A keyframe sweeps it along the blade on each change and three things read it: the clip-path that shows the new side up to that point, two radial-gradient masks that pinch the blade there as if seen edge on, and a shade band.
- Both sides are printed with repeating gradients for the ticks, centimetres down to millimetres and inches down to eighths, and numbered spans.
Accessibility: A role="radiogroup" labelled "Units" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The options read Metric and Imperial and focus outlines them; the tape measure is aria-hidden. Under prefers-reduced-motion the sides swap 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 "Units" radio.
Visual behavior: Metric or imperial on a tape measure: the blade twists over from the case to the hook to show its other side, with a pinch running along it where it turns.
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.
- One registered custom property (@property, <length>) is the position of the twist. A keyframe sweeps it along the blade on each change and three things read it: the clip-path that shows the new side up to that point, two radial-gradient masks that pinch the blade there as if seen edge on, and a shade band.
- Both sides are printed with repeating gradients for the ticks, centimetres down to millimetres and inches down to eighths, and numbered spans.
Accessibility: A role="radiogroup" labelled "Units" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The options read Metric and Imperial and focus outlines them; the tape measure is aria-hidden. Under prefers-reduced-motion the sides swap 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
Metric or imperial on a tape measure: the blade twists over from the case to the hook to show its other side, with a pinch running along it where it turns.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- click, state change, keyframe, morph
- Techniques
- css property, clip path, keyframes, radial gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" labelled "Units" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The options read Metric and Imperial and focus outlines them; the tape measure is aria-hidden. Under prefers-reduced-motion the sides swap at once.