A complete "enter your sign-in code" card, lit from below like a fire. Each slot has a small coal at its foot. When you type, the digit catches white-hot, throws a few sparks and cools to a warm glow. While the code is checked, heat runs along the row. A wrong code puts the coals out: the digits go grey and crumble away as ash. A right one makes every digit flare and burn off, and a spark etches a check that cools to gold before a Continue button appears.
Good for
Sign-in codes, two-step verification, email and phone confirmation, any screen that asks for a one-time code.
How it works
One real input sits invisibly over the slots, so paste, SMS and email autofill and screen readers work the usual way. Your check runs as soon as the code is complete, and its answer picks the ending. A resend button counts down before it can be pressed.
How to use
Copy two files, paste the markup, pass your own onComplete(code). Four or eight digits: add or remove slots.
Specs
Price
$5, one payment
Version
1.0.0
Code size
9.3 KB gzipped
Dependencies
None
Built with
HTML, CSS, JavaScript
Category
Form
Licence
Unlimited personal and client projects
In the zip
ember.cssember.jsthe component
index.htmla plain example page
README.mdpackage.jsonvite.config.jsLICENSE.md
Use it in your project
Copy ember.css and ember.js into your project.
Add the markup from index.html (the block between the COMPONENT comments). One .ember-slot per digit: six are in the example, four or eight work the same.
Start it and connect your own check:
import { init } from './ember.js';
const ember = init(document.querySelector('.ember'), {
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 field
},
onResend: () => fetch('/api/send-code', { method: 'POST' }),
onContinue: () => { location.href = '/dashboard'; },
});
onComplete runs as soon as the last digit is in. Return false or throw to show an error: the digits go grey and crumble away as ash so the user can type again. Resolve and the digits burn off, a spark etches a check that cools to gold, and a Continue button appears.
There is one real <input> under the slots, 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.
Does autofill work?
Yes. It's a normal input with autocomplete="one-time-code", so phones offer the code from the message, and the digits light 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 signed in. With reduced motion there are no sparks or ash.
Can I turn the sparks off?
Yes, sparks: false keeps the glow and drops the particles.
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.
<!-- 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
<!-- 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)', …
A split-flap code field that clacks each digit down and flips the board to DONE
7.8 KB0 depsForms
index.html
<!-- the shackle sits
outside the body, so
it lifts and swings --><svg class="hasp-shackle" …><div class="hasp-body"><!-- one real input over
all four wheels --><input class="hasp-input"
…><span class="hasp-wheel" aria-hidden="true">
hasp.css
/* the wheel clips a strip:
--hasp-at picks the row,
a spring rolls it in */
.hasp-strip {
transform: translateY(
calc(var(--hasp-at) *
var(--hasp-wheel-h)
* -1));
transition: transform
var(--hasp-roll, 420ms)
var(--hasp-glide);
hasp.js
// the further it has to go,// the longer: 40ms a rowconst dist = Math.abs(
at - from);
strip.style.setProperty(
'--hasp-roll',
`${Math.min(640,
240 + dist * 40)}ms`);
// then just name the row
strip.style.setProperty(
'--hasp-at', at);
A combination-padlock code field whose wheels roll and whose shackle springs open
7.7 KB0 depsForms
index.html
<!-- clip ends at the hole
line, so the bar sinks
into the case --><g clip-path=
"url(#latch-clip-above)"><g class="latch-shackle"><g class="latch-bar"><!-- one d, four widths: a
round polished tube --><path class="latch-tube
latch-tube-1" d="…"/>
latch.css
/* pivot on the long leg,
in SVG viewBox units */
.latch-shackle, .latch-bar {
transform-box: view-box;
transform-origin:
69px30px; }
/* scaleX fakes the turn;
stroke width holds */
.latch-tube { …
vector-effect:
non-scaling-stroke; }
latch.js
// v is kept on retarget, so// a spin never jerks
v += (-k * (x - target)
- c * v) * (dt / 4);
x += v * (dt / 4);
…
// speed blurs the digitsconst b = Math.min(2.6,
(Math.abs(v)*cell) / 900);
if (b > 0.25) { strip.style
.filter = `blur(${b…}px)`;