A complete "enter the code we sent you" card with six tiles in a tray, three up and three down, like a small sliding puzzle. A glowing ring glides to the tile that takes the next digit, and each digit stamps down onto its tile. While the code is checked, a light runs along the top row and back along the bottom. A wrong code makes each tile try to slide on to the next slot, knock against its neighbour and bounce back red while the digits drop out. A right one turns the tiles over, one after another, to show a picture in the wrong order. One tile lifts out of the tray, the rest slide through the gap one at a time until the picture is whole, the spare tile drops back in, and the gaps close into one green plate with a check. Then a Continue button rises.
Good for
Two-step sign-in, phone and email confirmation, account recovery, any screen that asks for a 6-digit code and deserves a moment at the end.
How it works
One real input sits invisibly over the tiles, so paste, SMS autofill and screen readers work the usual way. Your check runs as soon as the sixth digit is in, and its answer picks the ending. The scramble is a real sequence of slides played backwards, so it always solves; set your own or ask for a random one each time. The picture is a plain SVG you can swap for your own mark. A resend button counts down before it can be pressed.
How to use
Copy two files, paste the markup, pass your own onComplete(code).
Specs
Price
$12, one payment
Version
1.0.0
Code size
10.7 KB gzipped
Dependencies
None
Built with
HTML, CSS, JavaScript
Category
Form
Licence
Unlimited personal and client projects
In the zip
shunt.cssshunt.jsthe component
index.htmla plain example page
README.mdpackage.jsonvite.config.jsLICENSE.md
Use it in your project
Copy shunt.css and shunt.js into your project.
Add the markup from index.html (the block between the COMPONENT comments). The grid is six empty .shunt-tiles, three up and three down; shunt.js builds their insides.
Start it and connect your own check:
import { init } from './shunt.js';
const shunt = init(document.querySelector('.shunt'), {
onComplete: async (code) => {
const res = await fetch('/api/verify', { method: 'POST', body: JSON.stringify({ code }) });
if (!res.ok) throw new Error('That code has expired. Send a new one.'); // shown under the grid
},
onResend: () => fetch('/api/send-code', { method: 'POST' }),
onContinue: () => { location.href = '/dashboard'; },
});
onComplete runs as soon as the sixth digit is in. Return false or throw to show an error: each tile tries to slide on to the next slot, knocks against its neighbour and bounces back red, and the digits drop out so the user can type again. Resolve and the tiles turn over, one after another, to show a picture in the wrong order. One tile lifts out of the tray, the others slide through the gap one at a time until the picture is whole, the spare tile drops back in, the gaps close into one green plate and a Continue button appears.
There is one real <input> under the tiles, so the things that normally work with a code field still do: paste, SMS and email autofill (autocomplete="one-time-code"), the numeric keyboard on phones, and screen readers.
The full source, an example page and a README. No account, no framework.
Can I use it in client projects?
Yes, in unlimited projects. You can't resell or share the source itself.
Does it check the code?
It calls your function with the code and shows the answer. Checking belongs on your server; the demo only compares against a fixed code.
Can I change the puzzle?
Yes. path sets the scramble as a walk of the empty slot (or a number for a random walk that long), tempo sets the pace of each slide, and the picture it solves into is an SVG in the markup you can replace with your own.
Does autofill from SMS work?
Yes. It's a normal input with autocomplete="one-time-code", so iOS and Android offer the code from the message, and the digits stamp in one after another.
Is it accessible?
One labelled field, numeric keyboard on phones, live status messages, aria-invalid on errors, and focus moves to Continue once verified. With reduced motion no tile turns or travels.
Light and dark?
Both are designed, and every colour is a CSS variable.
What if it doesn't work for me?
If it doesn't work like the demo, you get a refund within 14 days.
Still have a question? Email us. The person who made it answers.
<!-- eyes before the head:
they sink behind it --><g class="tuck-eye
tuck-eye--back">
…
<path class="tuck-body" …/><!-- the engine finds the
password by this --><input class="tuck-input
tuck-input--secret" …
data-mascot="secret" …>
tuck.css
/* hide: stalks shrink to
their base, the eyes sink
behind the head */
.tuck[data-mood="hide"]
.tuck-stalk, …
{ transform: scaleY(0); …
.tuck[data-mood="hide"]
.tuck-eye--front, …
{ transform:
translate(4px, 54px); …
mascot.js
// the eyes chase the caret:// width of the text before// it, in the input's font
ctx.font = `${cs.fontWeight}
${cs.fontSize} …`;
…
// scrolled fields shift the// text; clamp to the field
x: clamp(left + ctx
.measureText(text).width
- input.scrollLeft, …
Sign-up, sign-in and reset forms with a snail that pulls its eyes in while you type your password
10.6 KB0 depsForms
index.html
<!-- one real input; js
draws each piece's
outline as an svg --><div class="dovetail-row"><input class="dovetail-input"
…><span class="dovetail-piece" aria-hidden="true"></span>
dovetail.css
/* js sets each slide's
length; a cut-off one
resumes where it is */
.dovetail-piece {
translate: 00;
transition:
translate
var(--dovetail-move)
var(--dovetail-glide), …
.dovetail-piece.is-lifted {
… scale: 1.06; }
A one-time code field of puzzle pieces that slide together and lock into a check
11 KB0 depsForms
index.html
<!-- a coal per slot; the
check has no d, js
fits it to the row;
sparks spawn in fx --><span class="ember-slot" …><span class="ember-wick"></span></span>
…
<svg class="ember-mark" …><path class="ember-check"/><span class="ember-fx" …>
ember.css
/* two copies of the digit:
a blurred one behind it
carries the heat */
.ember-num, .ember-heat {
grid-area: 1 / 1;
…
.ember-heat {
color: var(--ember-flame);
filter: blur(7px);
opacity: .3; …
ember.js
// one eased k draws the// stroke and puts the spark// at its tip, via along(k)const k = easeInOut(
Math.min(1, (now - t0)
/ dur));
const [x, y] = along(k);
… p.style.strokeDashoffset
= String(1 - k);
… c.setAttribute('cx', x);
A one-time code field where each digit lights like an ember and a wrong code turns to ash
9.3 KB0 depsForms
index.html
<!-- one real input on top;
the tiles stay empty,
flap.js adds halves
and a hinged leaf --><div class="flap-row"><input class="flap-input" id="otp" type="text" …><span class="flap-tile" aria-hidden="true"></span>
…
flap.css
/* one leaf, two faces:
the back waits folded
behind the front */
.flap-tile {
perspective: 480px; …
.flap-leaf-front,
.flap-leaf-back {
backface-visibility:
hidden; …
transform:
rotateX(180deg); }
flap.js
// new glyph: top half and// leaf back; the old one// stays on the leaf frontpaint(c.top, glyph, tone);
paint(c.back, glyph, tone);
// fall to -180, bounce 5°// back and settle: clackconst a = c.leaf.animate(
… 'rotateX(-180deg)',
offset: 0.86, …
'rotateX(-175deg)', …