- movin shell, type help
Scramble
import { useState } from 'react';
const css = `
@keyframes field-scramble-dot { from { transform: scale(0.2); } 60% { transform: scale(1.3); } }
@keyframes field-scramble-shake { 20%, 60% { transform: translateX(-4px); } 40%, 80% { transform: translateX(4px); } }
.field-scramble { width: 170px; }
.field-scramble__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.field-scramble__label { color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-scramble__dots { display: flex; gap: 7px; }
.field-scramble__dots[data-done] { animation: field-scramble-shake 0.4s ease-in-out; }
.field-scramble__dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px light-dark(#6d7179, #7d8087); }
.field-scramble__dot[data-on] { background: light-dark(#373a40, #f1f2f3); box-shadow: none; animation: field-scramble-dot 0.25s ease-out; }
.field-scramble__pad { user-select: none; -webkit-user-select: none; position: relative; height: 172px; }
.field-scramble__key {
position: absolute;
left: 0;
top: 0;
width: 52px;
height: 38px;
border: 0;
border-radius: 9px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 -2px 0 light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit; font-size: 16px; font-weight: 600;
cursor: pointer;
/* Keys glide to their new slots after every press. */
transition: transform 0.38s cubic-bezier(0.34, 1.3, 0.64, 1), background-color 0.15s;
}
.field-scramble__key:active { background: light-dark(#cbced3, #393c41); }
.field-scramble__key:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 1px; }
.field-scramble__key[data-fn] { background: none; box-shadow: none; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-scramble__status { min-height: 16px; margin-top: 4px; color: light-dark(#50535b, #c6c8cd); font-size: 11px; text-align: center; }
@media (prefers-reduced-motion: reduce) { .field-scramble__key { transition: background-color 0.15s; } .field-scramble__dots[data-done], .field-scramble__dot[data-on] { animation: none; } }
.field-scramble__top { display: flex; align-items: center; gap: 5px; margin-bottom: 6px; }
.field-scramble__top > :first-child { margin: 0; }
.field-scramble__info { position: relative; display: grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: none; color: light-dark(#5f626a, #a7abb2); cursor: help; }
.field-scramble__info svg { width: 14px; height: 14px; }
.field-scramble__info:hover { color: light-dark(#42454c, #e5e6e9); }
.field-scramble__info:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 1px; }
/* How-to lives behind the icon, so the field itself stays minimal. */
.field-scramble__info::after {
content: attr(data-tip);
position: absolute;
left: -6px;
bottom: calc(100% + 6px);
z-index: 5;
padding: 4px 8px;
border-radius: 6px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font-family: inherit; font-size: 11px; font-weight: 500; line-height: 1.3;
width: max-content;
max-width: 150px;
opacity: 0;
transform: translateY(3px);
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.field-scramble__info:hover::after, .field-scramble__info:focus-visible::after { opacity: 1; transform: none; }
`;
const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const DIGIT_SLOTS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10];
const slot = (i: number) => `translate(${(i % 3) * 59}px, ${Math.floor(i / 3) * 44}px)`;
const shuffle = () => {
const a = [...DIGITS];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
export function ScrambleInput() {
// order[k] is the digit shown in the k-th digit slot. Starts in phone order.
const [order, setOrder] = useState(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']);
const [pin, setPin] = useState('');
const done = pin.length === 4;
const press = (d: string) => {
if (done) return;
const next = pin + d;
setPin(next);
// Reshuffle so the finger pattern gives nothing away.
setOrder(shuffle());
if (next.length === 4) window.setTimeout(() => setPin(''), 1600);
};
return (
<>
<style>
{css}
</style>
<div className="field-scramble" role="group" aria-labelledby="field-scramble-label">
<div className="field-scramble__head">
<div className="field-scramble__top">
<span id="field-scramble-label" className="field-scramble__label">
PIN
</span>
<button type="button" className="field-scramble__info" aria-label="Keys shuffle after each press" data-tip="Keys shuffle after each press">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5M12 7.5v.01" />
</svg>
</button>
</div>
<span className="field-scramble__dots" data-done={done || undefined} aria-label={`${pin.length} of 4 digits entered`} role="img">
{[0, 1, 2, 3].map((i) => (
<span key={i} className="field-scramble__dot" data-on={i < pin.length || undefined} />
))}
</span>
</div>
<div className="field-scramble__pad">
{DIGITS.map((d) => (
<button key={d} type="button" className="field-scramble__key" style={{ transform: slot(DIGIT_SLOTS[order.indexOf(d)]) }} onClick={() => press(d)}>
{d}
</button>
))}
<button type="button" className="field-scramble__key" data-fn style={{ transform: slot(9) }} onClick={() => setPin('')}>
Clear
</button>
<button type="button" className="field-scramble__key" data-fn aria-label="Delete" style={{ transform: slot(11) }} onClick={() => setPin((p) => p.slice(0, -1))}>
⌫
</button>
</div>
<div className="field-scramble__status" aria-live="polite">
{done ? 'PIN entered' : ''}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldScramble(el) on the mounted element instead.
<div class="field-scramble" role="group" aria-labelledby="field-scramble-label">
<div class="field-scramble__head">
<div class="field-scramble__top">
<span id="field-scramble-label" class="field-scramble__label">PIN</span>
<button type="button" class="field-scramble__info" aria-label="Keys shuffle after each press" data-tip="Keys shuffle after each press">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 11v5M12 7.5v.01"></path>
</svg>
</button>
</div>
<span class="field-scramble__dots" aria-label="0 of 4 digits entered" role="img">
<span class="field-scramble__dot"></span>
<span class="field-scramble__dot"></span>
<span class="field-scramble__dot"></span>
<span class="field-scramble__dot"></span>
</span>
</div>
<div class="field-scramble__pad">
<button type="button" class="field-scramble__key" style="transform:translate(59px, 132px)">0</button>
<button type="button" class="field-scramble__key" style="transform:translate(0px, 0px)">1</button>
<button type="button" class="field-scramble__key" style="transform:translate(59px, 0px)">2</button>
<button type="button" class="field-scramble__key" style="transform:translate(118px, 0px)">3</button>
<button type="button" class="field-scramble__key" style="transform:translate(0px, 44px)">4</button>
<button type="button" class="field-scramble__key" style="transform:translate(59px, 44px)">5</button>
<button type="button" class="field-scramble__key" style="transform:translate(118px, 44px)">6</button>
<button type="button" class="field-scramble__key" style="transform:translate(0px, 88px)">7</button>
<button type="button" class="field-scramble__key" style="transform:translate(59px, 88px)">8</button>
<button type="button" class="field-scramble__key" style="transform:translate(118px, 88px)">9</button>
<button type="button" class="field-scramble__key" data-fn="true" style="transform:translate(0px, 132px)">Clear</button>
<button type="button" class="field-scramble__key" data-fn="true" aria-label="Delete" style="transform:translate(118px, 132px)">⌫</button>
</div>
<div class="field-scramble__status" aria-live="polite"></div>
</div>@keyframes field-scramble-dot { from { transform: scale(0.2); } 60% { transform: scale(1.3); } }
@keyframes field-scramble-shake { 20%, 60% { transform: translateX(-4px); } 40%, 80% { transform: translateX(4px); } }
.field-scramble { width: 170px; }
.field-scramble__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.field-scramble__label { color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-scramble__dots { display: flex; gap: 7px; }
.field-scramble__dots[data-done] { animation: field-scramble-shake 0.4s ease-in-out; }
.field-scramble__dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px light-dark(#6d7179, #7d8087); }
.field-scramble__dot[data-on] { background: light-dark(#373a40, #f1f2f3); box-shadow: none; animation: field-scramble-dot 0.25s ease-out; }
.field-scramble__pad { user-select: none; -webkit-user-select: none; position: relative; height: 172px; }
.field-scramble__key {
position: absolute;
left: 0;
top: 0;
width: 52px;
height: 38px;
border: 0;
border-radius: 9px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 -2px 0 light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit; font-size: 16px; font-weight: 600;
cursor: pointer;
/* Keys glide to their new slots after every press. */
transition: transform 0.38s cubic-bezier(0.34, 1.3, 0.64, 1), background-color 0.15s;
}
.field-scramble__key:active { background: light-dark(#cbced3, #393c41); }
.field-scramble__key:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 1px; }
.field-scramble__key[data-fn] { background: none; box-shadow: none; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-scramble__status { min-height: 16px; margin-top: 4px; color: light-dark(#50535b, #c6c8cd); font-size: 11px; text-align: center; }
@media (prefers-reduced-motion: reduce) { .field-scramble__key { transition: background-color 0.15s; } .field-scramble__dots[data-done], .field-scramble__dot[data-on] { animation: none; } }
.field-scramble__top { display: flex; align-items: center; gap: 5px; margin-bottom: 6px; }
.field-scramble__top > :first-child { margin: 0; }
.field-scramble__info { position: relative; display: grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: none; color: light-dark(#5f626a, #a7abb2); cursor: help; }
.field-scramble__info svg { width: 14px; height: 14px; }
.field-scramble__info:hover { color: light-dark(#42454c, #e5e6e9); }
.field-scramble__info:focus-visible { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 1px; }
/* How-to lives behind the icon, so the field itself stays minimal. */
.field-scramble__info::after {
content: attr(data-tip);
position: absolute;
left: -6px;
bottom: calc(100% + 6px);
z-index: 5;
padding: 4px 8px;
border-radius: 6px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font-family: inherit; font-size: 11px; font-weight: 500; line-height: 1.3;
width: max-content;
max-width: 150px;
opacity: 0;
transform: translateY(3px);
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.field-scramble__info:hover::after, .field-scramble__info:focus-visible::after { opacity: 1; transform: none; }function initFieldScramble(root) {
var keys = root.querySelectorAll('.field-scramble__key');
var dotsEl = root.querySelector('.field-scramble__dots');
var dots = root.querySelectorAll('.field-scramble__dot');
var status = root.querySelector('.field-scramble__status');
if (keys.length !== 12 || !dotsEl || dots.length !== 4 || !status) return;
var DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
var DIGIT_SLOTS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10];
var digitKeys = Array.prototype.slice.call(keys, 0, 10);
var clear = keys[10];
var del = keys[11];
var pin = '';
var timer = 0;
function slot(i) { return 'translate(' + (i % 3) * 59 + 'px, ' + Math.floor(i / 3) * 44 + 'px)'; }
function shuffle() {
var a = DIGITS.slice();
for (var i = a.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = a[i];
a[i] = a[j];
a[j] = t;
}
return a;
}
function render() {
var done = pin.length === 4;
dots.forEach(function (d, i) { d.toggleAttribute('data-on', i < pin.length); });
dotsEl.toggleAttribute('data-done', done);
dotsEl.setAttribute('aria-label', pin.length + ' of 4 digits entered');
status.textContent = done ? 'PIN entered' : '';
}
function press(e) {
if (pin.length === 4) return;
pin += e.currentTarget.textContent.trim();
// Reshuffle so the finger pattern gives nothing away.
var order = shuffle();
digitKeys.forEach(function (k) { k.style.transform = slot(DIGIT_SLOTS[order.indexOf(k.textContent.trim())]); });
if (pin.length === 4) timer = setTimeout(function () { pin = ''; render(); }, 1600);
render();
}
function reset() { pin = ''; render(); }
function back() { pin = pin.slice(0, -1); render(); }
digitKeys.forEach(function (k) { k.addEventListener('click', press); });
clear.addEventListener('click', reset);
del.addEventListener('click', back);
return function () {
clearTimeout(timer);
digitKeys.forEach(function (k) { k.removeEventListener('click', press); });
clear.removeEventListener('click', reset);
del.removeEventListener('click', back);
};
}
document.querySelectorAll('.field-scramble').forEach(initFieldScramble);Goal: Create a "Scramble" input.
Visual behavior: A PIN pad whose keys glide to new places after every press, so the pattern of taps gives nothing away. Four dots fill as you go, and a full PIN shakes the dots and resets.
Technique:
- Every key is absolutely placed by a transform computed from its slot in a 3 x 4 grid; Clear and Delete keep fixed slots at the bottom.
- After each digit the ten digits are shuffled (Fisher-Yates) and every key transitions to its new slot on a slightly overshooting curve.
- The dots fill with a pop; at four digits they shake and reset after a moment.
Accessibility: Every key is a real button labelled with its digit; the dots carry an aria-label with the count and the status is aria-live. Under prefers-reduced-motion keys jump without gliding.
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 "Scramble" input.
Visual behavior: A PIN pad whose keys glide to new places after every press, so the pattern of taps gives nothing away. Four dots fill as you go, and a full PIN shakes the dots and resets.
Technique:
- Every key is absolutely placed by a transform computed from its slot in a 3 x 4 grid; Clear and Delete keep fixed slots at the bottom.
- After each digit the ten digits are shuffled (Fisher-Yates) and every key transitions to its new slot on a slightly overshooting curve.
- The dots fill with a pop; at four digits they shake and reset after a moment.
Accessibility: Every key is a real button labelled with its digit; the dots carry an aria-label with the count and the status is aria-live. Under prefers-reduced-motion keys jump without gliding.
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 PIN pad whose keys glide to new places after every press, so the pattern of taps gives nothing away. Four dots fill as you go, and a full PIN shakes the dots and resets.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, spring, state change
- Techniques
- css transition, keyframes, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Every key is a real button labelled with its digit; the dots carry an aria-label with the count and the status is aria-live. Under prefers-reduced-motion keys jump without gliding.