Key
import { useId, useState, type CSSProperties } from 'react';
const LEN = 6;
const CUT = (i: number) => 16 + i * 24; // notch centres along the blade
// The blade's outline: a V notch per digit, as deep as the digit, then the tip.
const blade = (code: string) => {
const pts = ['0px 0px'];
for (let i = 0; i < LEN; i++) {
const d = i < code.length ? 3 + Number(code[i]) * 1.6 : 0;
pts.push(`${CUT(i) - 8}px 0px`, `${CUT(i)}px ${d.toFixed(1)}px`, `${CUT(i) + 8}px 0px`);
}
pts.push('150px 0px', '160px 14px', '156px 28px', '0px 28px');
return `polygon(${pts.join(', ')})`;
};
const css = `
.field-key { --steel-hi: light-dark(#fbfcfd, #e5e6e9); --steel: light-dark(#dbdde1, #a7abb2); --steel-lo: light-dark(#6d7179, #393c41); display: grid; justify-items: center; }
.field-key__bench { position: relative; display: grid; justify-items: center; gap: 6px; width: 262px; cursor: text; }
.field-key__rig { position: relative; width: 262px; height: 84px; }
.field-key__input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: transparent; font-size: 16px; outline: none; cursor: text; }
.field-key__input::selection { background: transparent; }
/* The key: a bow with its hole, and a blade whose top edge is cut by clip-path. */
.field-key__key { position: absolute; left: 6px; top: 16px; width: 206px; height: 48px; transition: translate 0.45s cubic-bezier(0.5, 0, 0.2, 1) 0.3s, scale 0.3s ease-in-out; filter: drop-shadow(0 4px 4px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.field-key__bow { position: absolute; left: 0; top: 0; width: 48px; height: 48px; border-radius: 50%; background: radial-gradient(circle, transparent 0 8px, var(--steel-lo) 8.5px 10px, transparent 10.5px), linear-gradient(160deg, var(--steel-hi), var(--steel) 55%, var(--steel-lo)); }
.field-key__neck { position: absolute; left: 40px; top: 14px; width: 12px; height: 20px; background: linear-gradient(var(--steel-hi), var(--steel)); }
.field-key__blade { position: absolute; left: 46px; top: 10px; width: 160px; height: 28px; clip-path: var(--cut); transition: clip-path 0.32s cubic-bezier(0.3, 0, 0.2, 1); background: linear-gradient(180deg, var(--steel-hi), var(--steel) 45%, var(--steel-lo) 60%, var(--steel) 64%, var(--steel) 80%, var(--steel-lo)); }
.field-key__digit { position: absolute; top: 18px; width: 10px; margin-left: -5px; color: var(--steel-lo); font: 600 7.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; }
.field-key__next { position: absolute; top: 44px; width: 0; height: 0; margin-left: -4px; border: 4px solid transparent; border-bottom: 5px solid light-dark(#6d7179, #7d8087); transition: left 0.3s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-key__bench:has(.field-key__input:focus) .field-key__next { border-bottom-color: light-dark(#373a40, #f1f2f3); }
/* The grinder drops onto the cut it is making, spinning, and throws sparks. */
.field-key__grinder { position: absolute; top: -10px; width: 30px; height: 30px; margin-left: -15px; border-radius: 50%; background: repeating-conic-gradient(var(--steel) 0 12deg, var(--steel-lo) 12deg 24deg); box-shadow: inset 0 0 0 4px var(--steel-lo); opacity: 0; animation: field-key-grind 0.42s cubic-bezier(0.3, 0, 0.3, 1); pointer-events: none; }
@keyframes field-key-grind { 0% { opacity: 0; transform: translateY(-10px) rotate(0); } 25%, 70% { opacity: 1; transform: translateY(0) rotate(240deg); } 100% { opacity: 0; transform: translateY(-12px) rotate(400deg); } }
.field-key__spark { position: absolute; top: 10px; width: 1.5px; height: 6px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; transform-origin: 50% 100%; animation: field-key-spark 0.4s ease-out 0.1s; pointer-events: none; }
@keyframes field-key-spark { 0% { opacity: 1; transform: rotate(var(--a)) translateY(0) scaleY(0.4); } 100% { opacity: 0; transform: rotate(var(--a)) translateY(-16px) scaleY(1); } }
/* The lock: a cylinder face the key slides into, whose keyway turns once it is in. */
.field-key__lock { position: absolute; right: 4px; top: 16px; z-index: 2; width: 46px; height: 48px; border-radius: 12px; background: linear-gradient(160deg, var(--steel), var(--steel-lo)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent), 0 6px 10px -4px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-key__cyl { position: absolute; left: 7px; top: 8px; width: 32px; height: 32px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, var(--steel-hi), var(--steel) 70%); box-shadow: 0 0 0 2px var(--steel-lo); transition: rotate 0.3s ease-in-out 0.75s; }
.field-key__cyl::after { content: ''; position: absolute; left: 50%; top: 6px; width: 4px; height: 20px; margin-left: -2px; border-radius: 2px; background: light-dark(#fbfcfd, #121417); }
.field-key[data-done] .field-key__key { translate: 34px 0; scale: 1 0.3; transition: translate 0.45s cubic-bezier(0.5, 0, 0.2, 1), scale 0.3s ease-in-out 0.75s; }
.field-key[data-done] .field-key__cyl { rotate: 90deg; }
/* The code panel: six cells, the next one waiting with a caret. */
.field-key__head { display: flex; justify-content: space-between; width: 214px; color: light-dark(#5f626a, #a7abb2); font: 500 11px/14px system-ui, sans-serif; }
.field-key__state { font-variant-numeric: tabular-nums; }
.field-key[data-done] .field-key__state { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.field-key__cells { display: flex; gap: 6px; border-radius: 9px; }
.field-key__bench:has(.field-key__input:focus-visible) .field-key__cells { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.field-key__cell { position: relative; display: grid; place-items: center; width: 31px; height: 38px; border-radius: 7px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); color: light-dark(#373a40, #f1f2f3); font: 600 17px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; transition: box-shadow 0.2s; }
.field-key__cell--next { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); }
.field-key__bench:has(.field-key__input:focus) .field-key__cell--next { box-shadow: inset 0 0 0 1.5px light-dark(#42454c, #e5e6e9), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); }
.field-key__digit-in { animation: field-key-digit 0.34s cubic-bezier(0.3, 1.5, 0.5, 1) both; }
@keyframes field-key-digit { from { opacity: 0; transform: translateY(7px) scale(0.6); } }
.field-key__caret { width: 2px; height: 18px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; }
.field-key__bench:has(.field-key__input:focus) .field-key__caret { animation: field-key-blink 1s steps(1) infinite; }
@keyframes field-key-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
/* Once the lock turns, the cells take it in one quick wave. */
.field-key[data-done] .field-key__cell { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); animation: field-key-ok 0.36s ease-out calc(0.8s + var(--i) * 45ms); }
@keyframes field-key-ok { 40% { transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) {
.field-key *, .field-key[data-done] * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const id = useId();
const [code, setCode] = useState('4172');
const done = code.length === LEN;
return (
<>
<style href="kit-inputs-key" precedence="kit">
{css}
</style>
<div className="field-key" data-done={done || undefined}>
<label className="field-key__bench" htmlFor={id}>
<span className="field-key__rig" aria-hidden="true">
<span className="field-key__key">
<span className="field-key__neck" />
<span className="field-key__bow" />
<span className="field-key__blade" style={{ '--cut': blade(code) } as CSSProperties} />
{[...code].map((d, i) => (
<span key={i} className="field-key__digit" style={{ left: 46 + CUT(i) }}>
{d}
</span>
))}
{!done && <span className="field-key__next" style={{ left: 46 + CUT(code.length) }} />}
{code.length > 0 && (
<span key={code.length} style={{ position: 'absolute', left: 46 + CUT(code.length - 1), top: 0 }}>
<span className="field-key__grinder" />
{[-70, -45, -20, 20, 45, 70].map((a) => (
<i key={a} className="field-key__spark" style={{ '--a': `${a}deg` } as CSSProperties} />
))}
</span>
)}
</span>
<span className="field-key__lock">
<span className="field-key__cyl" />
</span>
</span>
<span className="field-key__head">
<span aria-hidden="true">Key code</span>
<span className="field-key__state" aria-live="polite">
{done ? 'Unlocked' : `${code.length} of ${LEN}`}
</span>
</span>
<span className="field-key__cells" aria-hidden="true">
{Array.from({ length: LEN }, (_, i) => (
<span key={i} className={`field-key__cell${i === code.length ? ' field-key__cell--next' : ''}`} style={{ '--i': i } as CSSProperties}>
{i < code.length ? (
<span key={code[i]} className="field-key__digit-in">
{code[i]}
</span>
) : (
i === code.length && <i className="field-key__caret" />
)}
</span>
))}
</span>
<input
id={id}
className="field-key__input"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={LEN}
aria-label="Key code, six digits"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, LEN))}
/>
</label>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldKey(el) on the mounted element instead.
<div class="field-key">
<label class="field-key__bench" for="field-key">
<span class="field-key__rig" aria-hidden="true">
<span class="field-key__key">
<span class="field-key__neck"></span>
<span class="field-key__bow"></span>
<span class="field-key__blade" style="--cut:polygon(0px 0px, 8px 0px, 16px 9.4px, 24px 0px, 32px 0px, 40px 4.6px, 48px 0px, 56px 0px, 64px 14.2px, 72px 0px, 80px 0px, 88px 6.2px, 96px 0px, 104px 0px, 112px 0.0px, 120px 0px, 128px 0px, 136px 0.0px, 144px 0px, 150px 0px, 160px 14px, 156px 28px, 0px 28px)"></span>
<span class="field-key__digit" style="left:62px">4</span>
<span class="field-key__digit" style="left:86px">1</span>
<span class="field-key__digit" style="left:110px">7</span>
<span class="field-key__digit" style="left:134px">2</span>
<span class="field-key__next" style="left:158px"></span>
<span style="position:absolute;left:134px;top:0">
<span class="field-key__grinder"></span>
<i class="field-key__spark" style="--a:-70deg"></i>
<i class="field-key__spark" style="--a:-45deg"></i>
<i class="field-key__spark" style="--a:-20deg"></i>
<i class="field-key__spark" style="--a:20deg"></i>
<i class="field-key__spark" style="--a:45deg"></i>
<i class="field-key__spark" style="--a:70deg"></i>
</span>
</span>
<span class="field-key__lock">
<span class="field-key__cyl"></span>
</span>
</span>
<span class="field-key__head">
<span aria-hidden="true">Key code</span>
<span class="field-key__state" aria-live="polite">4 of 6</span>
</span>
<span class="field-key__cells" aria-hidden="true">
<span class="field-key__cell" style="--i:0">
<span class="field-key__digit-in">4</span>
</span>
<span class="field-key__cell" style="--i:1">
<span class="field-key__digit-in">1</span>
</span>
<span class="field-key__cell" style="--i:2">
<span class="field-key__digit-in">7</span>
</span>
<span class="field-key__cell" style="--i:3">
<span class="field-key__digit-in">2</span>
</span>
<span class="field-key__cell field-key__cell--next" style="--i:4">
<i class="field-key__caret"></i>
</span>
<span class="field-key__cell" style="--i:5"></span>
</span>
<input id="field-key" class="field-key__input" inputMode="numeric" autoComplete="one-time-code" maxLength="6" aria-label="Key code, six digits" value="4172"/>
</label>
</div>.field-key { --steel-hi: light-dark(#fbfcfd, #e5e6e9); --steel: light-dark(#dbdde1, #a7abb2); --steel-lo: light-dark(#6d7179, #393c41); display: grid; justify-items: center; }
.field-key__bench { position: relative; display: grid; justify-items: center; gap: 6px; width: 262px; cursor: text; }
.field-key__rig { position: relative; width: 262px; height: 84px; }
.field-key__input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: transparent; font-size: 16px; outline: none; cursor: text; }
.field-key__input::selection { background: transparent; }
/* The key: a bow with its hole, and a blade whose top edge is cut by clip-path. */
.field-key__key { position: absolute; left: 6px; top: 16px; width: 206px; height: 48px; transition: translate 0.45s cubic-bezier(0.5, 0, 0.2, 1) 0.3s, scale 0.3s ease-in-out; filter: drop-shadow(0 4px 4px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.field-key__bow { position: absolute; left: 0; top: 0; width: 48px; height: 48px; border-radius: 50%; background: radial-gradient(circle, transparent 0 8px, var(--steel-lo) 8.5px 10px, transparent 10.5px), linear-gradient(160deg, var(--steel-hi), var(--steel) 55%, var(--steel-lo)); }
.field-key__neck { position: absolute; left: 40px; top: 14px; width: 12px; height: 20px; background: linear-gradient(var(--steel-hi), var(--steel)); }
.field-key__blade { position: absolute; left: 46px; top: 10px; width: 160px; height: 28px; clip-path: var(--cut); transition: clip-path 0.32s cubic-bezier(0.3, 0, 0.2, 1); background: linear-gradient(180deg, var(--steel-hi), var(--steel) 45%, var(--steel-lo) 60%, var(--steel) 64%, var(--steel) 80%, var(--steel-lo)); }
.field-key__digit { position: absolute; top: 18px; width: 10px; margin-left: -5px; color: var(--steel-lo); font: 600 7.5px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; text-align: center; }
.field-key__next { position: absolute; top: 44px; width: 0; height: 0; margin-left: -4px; border: 4px solid transparent; border-bottom: 5px solid light-dark(#6d7179, #7d8087); transition: left 0.3s cubic-bezier(0.3, 1.4, 0.5, 1); }
.field-key__bench:has(.field-key__input:focus) .field-key__next { border-bottom-color: light-dark(#373a40, #f1f2f3); }
/* The grinder drops onto the cut it is making, spinning, and throws sparks. */
.field-key__grinder { position: absolute; top: -10px; width: 30px; height: 30px; margin-left: -15px; border-radius: 50%; background: repeating-conic-gradient(var(--steel) 0 12deg, var(--steel-lo) 12deg 24deg); box-shadow: inset 0 0 0 4px var(--steel-lo); opacity: 0; animation: field-key-grind 0.42s cubic-bezier(0.3, 0, 0.3, 1); pointer-events: none; }
@keyframes field-key-grind { 0% { opacity: 0; transform: translateY(-10px) rotate(0); } 25%, 70% { opacity: 1; transform: translateY(0) rotate(240deg); } 100% { opacity: 0; transform: translateY(-12px) rotate(400deg); } }
.field-key__spark { position: absolute; top: 10px; width: 1.5px; height: 6px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; transform-origin: 50% 100%; animation: field-key-spark 0.4s ease-out 0.1s; pointer-events: none; }
@keyframes field-key-spark { 0% { opacity: 1; transform: rotate(var(--a)) translateY(0) scaleY(0.4); } 100% { opacity: 0; transform: rotate(var(--a)) translateY(-16px) scaleY(1); } }
/* The lock: a cylinder face the key slides into, whose keyway turns once it is in. */
.field-key__lock { position: absolute; right: 4px; top: 16px; z-index: 2; width: 46px; height: 48px; border-radius: 12px; background: linear-gradient(160deg, var(--steel), var(--steel-lo)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 25%, transparent), 0 6px 10px -4px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-key__cyl { position: absolute; left: 7px; top: 8px; width: 32px; height: 32px; border-radius: 50%; background: radial-gradient(circle at 40% 35%, var(--steel-hi), var(--steel) 70%); box-shadow: 0 0 0 2px var(--steel-lo); transition: rotate 0.3s ease-in-out 0.75s; }
.field-key__cyl::after { content: ''; position: absolute; left: 50%; top: 6px; width: 4px; height: 20px; margin-left: -2px; border-radius: 2px; background: light-dark(#fbfcfd, #121417); }
.field-key[data-done] .field-key__key { translate: 34px 0; scale: 1 0.3; transition: translate 0.45s cubic-bezier(0.5, 0, 0.2, 1), scale 0.3s ease-in-out 0.75s; }
.field-key[data-done] .field-key__cyl { rotate: 90deg; }
/* The code panel: six cells, the next one waiting with a caret. */
.field-key__head { display: flex; justify-content: space-between; width: 214px; color: light-dark(#5f626a, #a7abb2); font: 500 11px/14px system-ui, sans-serif; }
.field-key__state { font-variant-numeric: tabular-nums; }
.field-key[data-done] .field-key__state { color: light-dark(#373a40, #f1f2f3); font-weight: 600; }
.field-key__cells { display: flex; gap: 6px; border-radius: 9px; }
.field-key__bench:has(.field-key__input:focus-visible) .field-key__cells { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.field-key__cell { position: relative; display: grid; place-items: center; width: 31px; height: 38px; border-radius: 7px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); color: light-dark(#373a40, #f1f2f3); font: 600 17px/1 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; transition: box-shadow 0.2s; }
.field-key__cell--next { box-shadow: inset 0 0 0 1px light-dark(#6d7179, #7d8087), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); }
.field-key__bench:has(.field-key__input:focus) .field-key__cell--next { box-shadow: inset 0 0 0 1.5px light-dark(#42454c, #e5e6e9), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); }
.field-key__digit-in { animation: field-key-digit 0.34s cubic-bezier(0.3, 1.5, 0.5, 1) both; }
@keyframes field-key-digit { from { opacity: 0; transform: translateY(7px) scale(0.6); } }
.field-key__caret { width: 2px; height: 18px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; }
.field-key__bench:has(.field-key__input:focus) .field-key__caret { animation: field-key-blink 1s steps(1) infinite; }
@keyframes field-key-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
/* Once the lock turns, the cells take it in one quick wave. */
.field-key[data-done] .field-key__cell { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), inset 0 2px 3px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.22)); animation: field-key-ok 0.36s ease-out calc(0.8s + var(--i) * 45ms); }
@keyframes field-key-ok { 40% { transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) {
.field-key *, .field-key[data-done] * { animation: none !important; transition: none !important; }
}function initFieldKey(root) {
var LEN = 6;
var key = root.querySelector('.field-key__key');
var blade = root.querySelector('.field-key__blade');
var next = root.querySelector('.field-key__next');
var state = root.querySelector('.field-key__state');
var cells = root.querySelectorAll('.field-key__cell');
var input = root.querySelector('.field-key__input');
var shown = [];
var cutAt = -1;
function cut(i) {
return 16 + i * 24;
}
// The blade's outline: a V notch per digit, as deep as the digit, then the tip.
function outline(code) {
var pts = ['0px 0px'];
for (var i = 0; i < LEN; i++) {
var d = i < code.length ? 3 + Number(code[i]) * 1.6 : 0;
pts.push(cut(i) - 8 + 'px 0px', cut(i) + 'px ' + d.toFixed(1) + 'px', cut(i) + 8 + 'px 0px');
}
pts.push('150px 0px', '160px 14px', '156px 28px', '0px 28px');
return 'polygon(' + pts.join(', ') + ')';
}
function render() {
var code = input.value;
var done = code.length === LEN;
root.toggleAttribute('data-done', done);
blade.style.setProperty('--cut', outline(code));
// The digits stamped on the blade.
key.querySelectorAll('.field-key__digit').forEach(function (d) {
d.remove();
});
code.split('').forEach(function (d, i) {
var s = document.createElement('span');
s.className = 'field-key__digit';
s.style.left = 46 + cut(i) + 'px';
s.textContent = d;
key.insertBefore(s, next);
});
next.hidden = done;
next.style.left = 46 + cut(code.length) + 'px';
// A fresh grinder and sparks over the notch just cut (or just filled back in).
if (code.length !== cutAt) {
var old = key.querySelector('.field-key__grinder');
if (old) old.parentNode.remove();
cutAt = code.length;
if (code.length) {
var at = document.createElement('span');
at.style.cssText = 'position:absolute;left:' + (46 + cut(code.length - 1)) + 'px;top:0';
at.innerHTML = '<span class="field-key__grinder"></span>' + [-70, -45, -20, 20, 45, 70].map(function (a) {
return '<i class="field-key__spark" style="--a:' + a + 'deg"></i>';
}).join('');
key.appendChild(at);
}
}
state.textContent = done ? 'Unlocked' : code.length + ' of ' + LEN;
// The cells: a digit pops in only when it changes; the next cell holds the caret.
cells.forEach(function (cell, i) {
cell.classList.toggle('field-key__cell--next', i === code.length);
var want = i < code.length ? code[i] : i === code.length ? '|' : '';
if (shown[i] === want) return;
shown[i] = want;
cell.textContent = '';
if (want === '|') {
var caret = document.createElement('i');
caret.className = 'field-key__caret';
cell.appendChild(caret);
} else if (want) {
var s = document.createElement('span');
s.className = 'field-key__digit-in';
s.textContent = want;
cell.appendChild(s);
}
});
}
function onInput() {
input.value = input.value.replace(/\D/g, '').slice(0, LEN);
render();
}
// The markup already shows the starting code; remember it so it is not replayed.
cutAt = input.value.length;
cells.forEach(function (cell, i) {
shown[i] = i < input.value.length ? input.value[i] : i === input.value.length ? '|' : '';
});
input.addEventListener('input', onInput);
return function () {
input.removeEventListener('input', onInput);
};
}
document.querySelectorAll('.field-key').forEach(initFieldKey);Goal: Create a "Key" input.
Visual behavior: A six-digit code cut into a key: each digit grinds a notch to its depth with sparks and drops into its cell below, and the finished key slides into the lock and turns.
Technique:
- One real <input> (inputmode numeric, autocomplete one-time-code, maxlength 6) lies transparent over the whole drawing and keeps only digits; the key, the cells and the status are drawn from its value.
- The blade is one element whose top edge is a clip-path polygon with a V notch per digit, as deep as the digit, so a new digit reshapes the outline and the clip-path transition grinds the cut in.
- A grinder disc and six sparks are mounted over the notch being cut and play once per digit.
- Under the key, six cells show the code. Each digit pops in on a spring, and the next empty cell waits with a blinking caret while the field has focus.
- At six digits a data-done attribute slides the key into the lock, squashes it on its long axis as it turns, rotates the cylinder a quarter turn and runs a small wave across the cells.
Accessibility: One native input named "Key code, six digits", transparent over the drawing so a click anywhere focuses it. Focus outlines the code cells and the next cell shows a caret. The count and "Unlocked" are read out from an aria-live line, and the key, lock and sparks are aria-hidden. Under prefers-reduced-motion the cut, key and lock change without moving.
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 "Key" input.
Visual behavior: A six-digit code cut into a key: each digit grinds a notch to its depth with sparks and drops into its cell below, and the finished key slides into the lock and turns.
Technique:
- One real <input> (inputmode numeric, autocomplete one-time-code, maxlength 6) lies transparent over the whole drawing and keeps only digits; the key, the cells and the status are drawn from its value.
- The blade is one element whose top edge is a clip-path polygon with a V notch per digit, as deep as the digit, so a new digit reshapes the outline and the clip-path transition grinds the cut in.
- A grinder disc and six sparks are mounted over the notch being cut and play once per digit.
- Under the key, six cells show the code. Each digit pops in on a spring, and the next empty cell waits with a blinking caret while the field has focus.
- At six digits a data-done attribute slides the key into the lock, squashes it on its long axis as it turns, rotates the cylinder a quarter turn and runs a small wave across the cells.
Accessibility: One native input named "Key code, six digits", transparent over the drawing so a click anywhere focuses it. Focus outlines the code cells and the next cell shows a caret. The count and "Unlocked" are read out from an aria-live line, and the key, lock and sparks are aria-hidden. Under prefers-reduced-motion the cut, key and lock change without moving.
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 six-digit code cut into a key: each digit grinds a notch to its depth with sparks and drops into its cell below, and the finished key slides into the lock and turns.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, focus, keyframe, spring
- Techniques
- clip path, keyframes, css transition, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
One native input named "Key code, six digits", transparent over the drawing so a click anywhere focuses it. Focus outlines the code cells and the next cell shows a caret. The count and "Unlocked" are read out from an aria-live line, and the key, lock and sparks are aria-hidden. Under prefers-reduced-motion the cut, key and lock change without moving.