Bolton your laptopHearton your laptopSmileon your laptopStaron your laptopGhoston your laptopFloweron your laptop
Sewing
React + Tailwind
import { useId, useState, type CSSProperties } from 'react';
// Each stitch is one repeating tile, `w` px long, drawn along y = 16.
const STITCHES = [
{ value: 'straight', label: 'Straight', w: 12, tile: (x: number) => `M${x + 2} 16h8` },
{ value: 'zigzag', label: 'Zigzag', w: 10, tile: (x: number) => `M${x} 10L${x + 5} 22L${x + 10} 10` },
{ value: 'cross', label: 'Cross', w: 14, tile: (x: number) => `M${x + 3} 10l8 12M${x + 11} 10l-8 12` },
{ value: 'scallop', label: 'Scallop', w: 16, tile: (x: number) => `M${x} 20q8 -15 16 0` },
];
const W = 262;
const NEEDLE = 176;
const SPEED = 36; // px per second, the fabric's feed
const run = (s: (typeof STITCHES)[number], from: number, to: number) => {
let d = '';
for (let x = from; x < to; x += s.w) d += s.tile(x);
return d;
};
const css = `
.radio-sewing { --cloth: light-dark(#dbdde1, #232529); --weave: color-mix(in oklab, var(--cloth) 82%, light-dark(#fbfcfd, #121417)); --thread: light-dark(#373a40, #f1f2f3); --steel: light-dark(#6d7179, #a7abb2); position: relative; width: ${W}px; height: 186px; }
/* The cloth feeds left under the foot; its weave moves with it. */
.radio-sewing__cloth { position: absolute; left: 0; right: 0; top: 54px; height: 66px; border-radius: 3px; background: repeating-linear-gradient(0deg, transparent 0 2px, var(--weave) 2px 3px), repeating-linear-gradient(90deg, transparent 0 2px, color-mix(in oklab, var(--weave) 60%, transparent) 2px 3px), var(--cloth); box-shadow: 0 8px 14px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); animation: radio-sewing-feed ${(24 / SPEED).toFixed(3)}s linear infinite; mask: linear-gradient(90deg, transparent, #000 16px, #000 calc(100% - 16px), transparent); }
@keyframes radio-sewing-feed { to { background-position: -24px 0, -24px 0, 0 0; } }
.radio-sewing__guide { position: absolute; left: ${NEEDLE}px; right: 12px; top: 87px; border-top: 1.2px dashed color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent); }
.radio-sewing__layer { position: absolute; left: 0; top: 71px; width: ${W}px; height: 32px; overflow: hidden; clip-path: inset(0 ${W - NEEDLE}px 0 0); display: none; }
.radio-sewing__layer svg { display: block; width: ${W + 40}px; height: 32px; overflow: visible; fill: none; stroke: var(--thread); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 1px 0 light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-sewing__layer g { animation: radio-sewing-run var(--t) linear infinite; }
@keyframes radio-sewing-run { to { transform: translateX(calc(var(--w) * -1px)); } }
.radio-sewing__layer--prev { display: block; z-index: 1; }
.radio-sewing__layer--cur { display: block; z-index: 2; }
/* The new stitch starts at the needle and its edge travels off with the cloth. */
.radio-sewing__layer--cur[data-n='0'] { animation: radio-sewing-start-a ${(NEEDLE / SPEED).toFixed(2)}s linear forwards; }
.radio-sewing__layer--cur[data-n='1'] { animation: radio-sewing-start-b ${(NEEDLE / SPEED).toFixed(2)}s linear forwards; }
/* ...while the old one is cut back at the same pace, so the two never overlap. */
.radio-sewing__layer--prev[data-n='0'] { animation: radio-sewing-end-a ${(NEEDLE / SPEED).toFixed(2)}s linear forwards; }
.radio-sewing__layer--prev[data-n='1'] { animation: radio-sewing-end-b ${(NEEDLE / SPEED).toFixed(2)}s linear forwards; }
@keyframes radio-sewing-end-a { from { clip-path: inset(0 ${W - NEEDLE}px 0 0); } to { clip-path: inset(0 ${W}px 0 0); } }
@keyframes radio-sewing-end-b { from { clip-path: inset(0 ${W - NEEDLE}px 0 0); } to { clip-path: inset(0 ${W}px 0 0); } }
@keyframes radio-sewing-start-a { from { clip-path: inset(0 ${W - NEEDLE}px 0 ${NEEDLE}px); } to { clip-path: inset(0 ${W - NEEDLE}px 0 0); } }
@keyframes radio-sewing-start-b { from { clip-path: inset(0 ${W - NEEDLE}px 0 ${NEEDLE}px); } to { clip-path: inset(0 ${W - NEEDLE}px 0 0); } }
/* The machine: needle bar, presser foot and a needle that goes down once per stitch. */
.radio-sewing__machine { position: absolute; left: 0; top: 0; width: ${W}px; height: 120px; overflow: visible; pointer-events: none; }
.radio-sewing__bar { fill: var(--steel); }
.radio-sewing__foot { fill: color-mix(in oklab, var(--steel) 85%, light-dark(#fbfcfd, #121417)); stroke: color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)); stroke-width: 0.8; }
.radio-sewing__needle { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.6; stroke-linecap: round; animation: radio-sewing-stab var(--t) cubic-bezier(0.45, 0, 0.55, 1) infinite; }
.radio-sewing__spool { stroke: var(--thread); stroke-width: 0.8; fill: none; opacity: 0.7; }
@keyframes radio-sewing-stab { 50% { transform: translateY(12px); } }
.radio-sewing__opts { position: absolute; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; gap: 6px; }
.radio-sewing__opt { position: relative; display: grid; justify-items: center; gap: 2px; width: 56px; padding: 6px 0 5px; border-radius: 6px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#6d7179, #7d8087); cursor: pointer; transition: background-color 0.25s, color 0.25s; }
.radio-sewing__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-sewing__opt:has(input:checked) { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087); }
.radio-sewing__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-sewing__opt::before { content: ''; position: absolute; right: 6px; top: 6px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: 0; transition: opacity 0.25s; }
.radio-sewing__opt:has(input:checked)::before { opacity: 1; }
.radio-sewing__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-sewing__glyph { width: 40px; height: 14px; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.radio-sewing__num { font: 600 8.5px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.06em; }
@media (prefers-reduced-motion: reduce) {
.radio-sewing *, .radio-sewing *::before { animation: none !important; transition: none !important; }
.radio-sewing__layer--cur { clip-path: inset(0 ${W - NEEDLE}px 0 0); }
}
`;
export function Preview() {
const name = useId();
const [s, setS] = useState({ cur: 1, prev: -1, n: 0 });
const pick = (i: number) => setS((v) => (v.cur === i ? v : { cur: i, prev: v.cur, n: v.n + 1 }));
const cur = STITCHES[s.cur];
return (
<>
<style href="kit-radios-sewing" precedence="kit">
{css}
</style>
<div className="radio-sewing" style={{ '--t': `${(cur.w / SPEED).toFixed(3)}s` } as CSSProperties}>
<span className="radio-sewing__cloth" aria-hidden="true" />
<span className="radio-sewing__guide" aria-hidden="true" />
{STITCHES.map((st, i) => (
<span
key={st.value}
className={`radio-sewing__layer${i === s.cur ? ' radio-sewing__layer--cur' : i === s.prev ? ' radio-sewing__layer--prev' : ''}`}
data-n={s.n % 2}
style={{ '--w': st.w, '--t': `${(st.w / SPEED).toFixed(3)}s` } as CSSProperties}
aria-hidden="true"
>
<svg viewBox={`0 0 ${W + 40} 32`}>
<g>
<path d={run(st, 0, W + 40)} />
</g>
</svg>
</span>
))}
<svg className="radio-sewing__machine" viewBox={`0 0 ${W} 120`} aria-hidden="true">
<path className="radio-sewing__spool" d={`M${NEEDLE + 9} 0V22L${NEEDLE + 1} 40`} />
<rect className="radio-sewing__bar" x={NEEDLE + 6} y={-4} width={7} height={52} rx={2} />
<rect className="radio-sewing__bar" x={NEEDLE - 4} y={8} width={8} height={16} rx={2} />
<line className="radio-sewing__needle" x1={NEEDLE} y1={22} x2={NEEDLE} y2={68} />
<path className="radio-sewing__foot" d={`M${NEEDLE - 18} 74Q${NEEDLE - 22} 74 ${NEEDLE - 22} 70Q${NEEDLE - 22} 64 ${NEEDLE - 14} 64H${NEEDLE - 3}V78H${NEEDLE + 3}V64H${NEEDLE + 20}Q${NEEDLE + 24} 64 ${NEEDLE + 24} 69V74ZM${NEEDLE + 7} 46H${NEEDLE + 12}V64H${NEEDLE + 7}Z`} />
</svg>
<div className="radio-sewing__opts" role="radiogroup" aria-label="Stitch">
{STITCHES.map((st, i) => (
<label key={st.value} className="radio-sewing__opt">
<input className="radio-sewing__input" type="radio" name={name} value={st.value} aria-label={st.label} checked={s.cur === i} onChange={() => pick(i)} />
<svg className="radio-sewing__glyph" viewBox="2 4 40 24" aria-hidden="true">
<path d={run(st, 2, 40)} />
</svg>
<span className="radio-sewing__num" aria-hidden="true">
{String(i + 1).padStart(2, '0')}
</span>
</label>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initRadioSewing(el) on the mounted element instead.
HTML
<div class="radio-sewing" style="--t:0.278s">
<span class="radio-sewing__cloth" aria-hidden="true"></span>
<span class="radio-sewing__guide" aria-hidden="true"></span>
<span class="radio-sewing__layer" data-n="0" style="--w:12;--t:0.333s" aria-hidden="true">
<svg viewBox="0 0 302 32">
<g>
<path d="M2 16h8M14 16h8M26 16h8M38 16h8M50 16h8M62 16h8M74 16h8M86 16h8M98 16h8M110 16h8M122 16h8M134 16h8M146 16h8M158 16h8M170 16h8M182 16h8M194 16h8M206 16h8M218 16h8M230 16h8M242 16h8M254 16h8M266 16h8M278 16h8M290 16h8M302 16h8"></path>
</g>
</svg>
</span>
<span class="radio-sewing__layer radio-sewing__layer--cur" data-n="0" style="--w:10;--t:0.278s" aria-hidden="true">
<svg viewBox="0 0 302 32">
<g>
<path d="M0 10L5 22L10 10M10 10L15 22L20 10M20 10L25 22L30 10M30 10L35 22L40 10M40 10L45 22L50 10M50 10L55 22L60 10M60 10L65 22L70 10M70 10L75 22L80 10M80 10L85 22L90 10M90 10L95 22L100 10M100 10L105 22L110 10M110 10L115 22L120 10M120 10L125 22L130 10M130 10L135 22L140 10M140 10L145 22L150 10M150 10L155 22L160 10M160 10L165 22L170 10M170 10L175 22L180 10M180 10L185 22L190 10M190 10L195 22L200 10M200 10L205 22L210 10M210 10L215 22L220 10M220 10L225 22L230 10M230 10L235 22L240 10M240 10L245 22L250 10M250 10L255 22L260 10M260 10L265 22L270 10M270 10L275 22L280 10M280 10L285 22L290 10M290 10L295 22L300 10M300 10L305 22L310 10"></path>
</g>
</svg>
</span>
<span class="radio-sewing__layer" data-n="0" style="--w:14;--t:0.389s" aria-hidden="true">
<svg viewBox="0 0 302 32">
<g>
<path d="M3 10l8 12M11 10l-8 12M17 10l8 12M25 10l-8 12M31 10l8 12M39 10l-8 12M45 10l8 12M53 10l-8 12M59 10l8 12M67 10l-8 12M73 10l8 12M81 10l-8 12M87 10l8 12M95 10l-8 12M101 10l8 12M109 10l-8 12M115 10l8 12M123 10l-8 12M129 10l8 12M137 10l-8 12M143 10l8 12M151 10l-8 12M157 10l8 12M165 10l-8 12M171 10l8 12M179 10l-8 12M185 10l8 12M193 10l-8 12M199 10l8 12M207 10l-8 12M213 10l8 12M221 10l-8 12M227 10l8 12M235 10l-8 12M241 10l8 12M249 10l-8 12M255 10l8 12M263 10l-8 12M269 10l8 12M277 10l-8 12M283 10l8 12M291 10l-8 12M297 10l8 12M305 10l-8 12"></path>
</g>
</svg>
</span>
<span class="radio-sewing__layer" data-n="0" style="--w:16;--t:0.444s" aria-hidden="true">
<svg viewBox="0 0 302 32">
<g>
<path d="M0 20q8 -15 16 0M16 20q8 -15 16 0M32 20q8 -15 16 0M48 20q8 -15 16 0M64 20q8 -15 16 0M80 20q8 -15 16 0M96 20q8 -15 16 0M112 20q8 -15 16 0M128 20q8 -15 16 0M144 20q8 -15 16 0M160 20q8 -15 16 0M176 20q8 -15 16 0M192 20q8 -15 16 0M208 20q8 -15 16 0M224 20q8 -15 16 0M240 20q8 -15 16 0M256 20q8 -15 16 0M272 20q8 -15 16 0M288 20q8 -15 16 0"></path>
</g>
</svg>
</span>
<svg class="radio-sewing__machine" viewBox="0 0 262 120" aria-hidden="true">
<path class="radio-sewing__spool" d="M185 0V22L177 40"></path>
<rect class="radio-sewing__bar" x="182" y="-4" width="7" height="52" rx="2"></rect>
<rect class="radio-sewing__bar" x="172" y="8" width="8" height="16" rx="2"></rect>
<line class="radio-sewing__needle" x1="176" y1="22" x2="176" y2="68"></line>
<path class="radio-sewing__foot" d="M158 74Q154 74 154 70Q154 64 162 64H173V78H179V64H196Q200 64 200 69V74ZM183 46H188V64H183Z"></path>
</svg>
<div class="radio-sewing__opts" role="radiogroup" aria-label="Stitch">
<label class="radio-sewing__opt">
<input class="radio-sewing__input" type="radio" aria-label="Straight" name="radio-sewing" value="straight"/>
<svg class="radio-sewing__glyph" viewBox="2 4 40 24" aria-hidden="true">
<path d="M4 16h8M16 16h8M28 16h8M40 16h8"></path>
</svg>
<span class="radio-sewing__num" aria-hidden="true">01</span>
</label>
<label class="radio-sewing__opt">
<input class="radio-sewing__input" type="radio" aria-label="Zigzag" name="radio-sewing" checked="" value="zigzag"/>
<svg class="radio-sewing__glyph" viewBox="2 4 40 24" aria-hidden="true">
<path d="M2 10L7 22L12 10M12 10L17 22L22 10M22 10L27 22L32 10M32 10L37 22L42 10"></path>
</svg>
<span class="radio-sewing__num" aria-hidden="true">02</span>
</label>
<label class="radio-sewing__opt">
<input class="radio-sewing__input" type="radio" aria-label="Cross" name="radio-sewing" value="cross"/>
<svg class="radio-sewing__glyph" viewBox="2 4 40 24" aria-hidden="true">
<path d="M5 10l8 12M13 10l-8 12M19 10l8 12M27 10l-8 12M33 10l8 12M41 10l-8 12"></path>
</svg>
<span class="radio-sewing__num" aria-hidden="true">03</span>
</label>
<label class="radio-sewing__opt">
<input class="radio-sewing__input" type="radio" aria-label="Scallop" name="radio-sewing" value="scallop"/>
<svg class="radio-sewing__glyph" viewBox="2 4 40 24" aria-hidden="true">
<path d="M2 20q8 -15 16 0M18 20q8 -15 16 0M34 20q8 -15 16 0"></path>
</svg>
<span class="radio-sewing__num" aria-hidden="true">04</span>
</label>
</div>
</div>CSS
.radio-sewing { --cloth: light-dark(#dbdde1, #232529); --weave: color-mix(in oklab, var(--cloth) 82%, light-dark(#fbfcfd, #121417)); --thread: light-dark(#373a40, #f1f2f3); --steel: light-dark(#6d7179, #a7abb2); position: relative; width: 262px; height: 186px; }
/* The cloth feeds left under the foot; its weave moves with it. */
.radio-sewing__cloth { position: absolute; left: 0; right: 0; top: 54px; height: 66px; border-radius: 3px; background: repeating-linear-gradient(0deg, transparent 0 2px, var(--weave) 2px 3px), repeating-linear-gradient(90deg, transparent 0 2px, color-mix(in oklab, var(--weave) 60%, transparent) 2px 3px), var(--cloth); box-shadow: 0 8px 14px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); animation: radio-sewing-feed 0.667s linear infinite; mask: linear-gradient(90deg, transparent, #000 16px, #000 calc(100% - 16px), transparent); }
@keyframes radio-sewing-feed { to { background-position: -24px 0, -24px 0, 0 0; } }
.radio-sewing__guide { position: absolute; left: 176px; right: 12px; top: 87px; border-top: 1.2px dashed color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent); }
.radio-sewing__layer { position: absolute; left: 0; top: 71px; width: 262px; height: 32px; overflow: hidden; clip-path: inset(0 86px 0 0); display: none; }
.radio-sewing__layer svg { display: block; width: 302px; height: 32px; overflow: visible; fill: none; stroke: var(--thread); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 1px 0 light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-sewing__layer g { animation: radio-sewing-run var(--t) linear infinite; }
@keyframes radio-sewing-run { to { transform: translateX(calc(var(--w) * -1px)); } }
.radio-sewing__layer--prev { display: block; z-index: 1; }
.radio-sewing__layer--cur { display: block; z-index: 2; }
/* The new stitch starts at the needle and its edge travels off with the cloth. */
.radio-sewing__layer--cur[data-n='0'] { animation: radio-sewing-start-a 4.89s linear forwards; }
.radio-sewing__layer--cur[data-n='1'] { animation: radio-sewing-start-b 4.89s linear forwards; }
/* ...while the old one is cut back at the same pace, so the two never overlap. */
.radio-sewing__layer--prev[data-n='0'] { animation: radio-sewing-end-a 4.89s linear forwards; }
.radio-sewing__layer--prev[data-n='1'] { animation: radio-sewing-end-b 4.89s linear forwards; }
@keyframes radio-sewing-end-a { from { clip-path: inset(0 86px 0 0); } to { clip-path: inset(0 262px 0 0); } }
@keyframes radio-sewing-end-b { from { clip-path: inset(0 86px 0 0); } to { clip-path: inset(0 262px 0 0); } }
@keyframes radio-sewing-start-a { from { clip-path: inset(0 86px 0 176px); } to { clip-path: inset(0 86px 0 0); } }
@keyframes radio-sewing-start-b { from { clip-path: inset(0 86px 0 176px); } to { clip-path: inset(0 86px 0 0); } }
/* The machine: needle bar, presser foot and a needle that goes down once per stitch. */
.radio-sewing__machine { position: absolute; left: 0; top: 0; width: 262px; height: 120px; overflow: visible; pointer-events: none; }
.radio-sewing__bar { fill: var(--steel); }
.radio-sewing__foot { fill: color-mix(in oklab, var(--steel) 85%, light-dark(#fbfcfd, #121417)); stroke: color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)); stroke-width: 0.8; }
.radio-sewing__needle { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.6; stroke-linecap: round; animation: radio-sewing-stab var(--t) cubic-bezier(0.45, 0, 0.55, 1) infinite; }
.radio-sewing__spool { stroke: var(--thread); stroke-width: 0.8; fill: none; opacity: 0.7; }
@keyframes radio-sewing-stab { 50% { transform: translateY(12px); } }
.radio-sewing__opts { position: absolute; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; gap: 6px; }
.radio-sewing__opt { position: relative; display: grid; justify-items: center; gap: 2px; width: 56px; padding: 6px 0 5px; border-radius: 6px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#6d7179, #7d8087); cursor: pointer; transition: background-color 0.25s, color 0.25s; }
.radio-sewing__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-sewing__opt:has(input:checked) { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087); }
.radio-sewing__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-sewing__opt::before { content: ''; position: absolute; right: 6px; top: 6px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: 0; transition: opacity 0.25s; }
.radio-sewing__opt:has(input:checked)::before { opacity: 1; }
.radio-sewing__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-sewing__glyph { width: 40px; height: 14px; overflow: visible; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.radio-sewing__num { font: 600 8.5px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; letter-spacing: 0.06em; }
@media (prefers-reduced-motion: reduce) {
.radio-sewing *, .radio-sewing *::before { animation: none !important; transition: none !important; }
.radio-sewing__layer--cur { clip-path: inset(0 86px 0 0); }
}JavaScript
function initRadioSewing(root) {
var layers = Array.prototype.slice.call(root.querySelectorAll('.radio-sewing__layer'));
var inputs = Array.prototype.slice.call(root.querySelectorAll('.radio-sewing__input'));
var cur = inputs.findIndex(function (i) { return i.checked; });
var n = 0;
// Keep the old stitch on screen while the new one starts at the needle;
// data-n alternates so the start animation replays on every pick.
function onChange(e) {
var i = inputs.indexOf(e.target);
if (i < 0 || i === cur) return;
var prev = cur;
cur = i;
n += 1;
layers.forEach(function (layer, k) {
layer.classList.toggle('radio-sewing__layer--cur', k === cur);
layer.classList.toggle('radio-sewing__layer--prev', k === prev);
layer.setAttribute('data-n', String(n % 2));
});
// The needle goes down once per stitch of the new pattern.
root.style.setProperty('--t', layers[cur].style.getPropertyValue('--t'));
}
root.addEventListener('change', onChange);
return function () {
root.removeEventListener('change', onChange);
};
}
document.querySelectorAll('.radio-sewing').forEach(initRadioSewing);Goal: Create a "Sewing" radio.
Visual behavior: A stitch picker on a sewing machine: the needle keeps sewing as the cloth feeds by, and the new stitch starts right at the needle while the old one travels off with the fabric.
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.
- Each stitch is one long SVG path of repeating tiles that slides left by one tile per period, so it moves with the cloth: its period is the tile length over the feed speed, and the needle stabs once per tile.
- A small state keeps the previous stitch. The new one is clipped to start at the needle and its edge moves left at the feed speed, while the old one is cut back from the same edge, so the seam changes stitch exactly where the needle was.
Accessibility: A role="radiogroup" labelled "Stitch" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its stitch, and focus outlines the option. The machine and cloth are aria-hidden. Under prefers-reduced-motion everything is still and the stitch is drawn up to the needle.
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 "Sewing" radio.
Visual behavior: A stitch picker on a sewing machine: the needle keeps sewing as the cloth feeds by, and the new stitch starts right at the needle while the old one travels off with the fabric.
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.
- Each stitch is one long SVG path of repeating tiles that slides left by one tile per period, so it moves with the cloth: its period is the tile length over the feed speed, and the needle stabs once per tile.
- A small state keeps the previous stitch. The new one is clipped to start at the needle and its edge moves left at the feed speed, while the old one is cut back from the same edge, so the seam changes stitch exactly where the needle was.
Accessibility: A role="radiogroup" labelled "Stitch" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its stitch, and focus outlines the option. The machine and cloth are aria-hidden. Under prefers-reduced-motion everything is still and the stitch is drawn up to the needle.
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 stitch picker on a sewing machine: the needle keeps sewing as the cloth feeds by, and the new stitch starts right at the needle while the old one travels off with the fabric.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- click, state change, continuous, loop
- Techniques
- clip path, keyframes, css variables, svg, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" labelled "Stitch" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its stitch, and focus outlines the option. The machine and cloth are aria-hidden. Under prefers-reduced-motion everything is still and the stitch is drawn up to the needle.