Field
Text entry in a recess, the label printed above it, and a hint that says how to fix an error.
Kit part · Forms · Inputs · Sign in & sign up
A PIN or one-time code dialled into a combination lock: number wheels behind a slot in the plate, turning on detents. Right, a locking bar drops into their notches; wrong, they spin back to blank.
Both finishes, side by side. Hover, pressed and focus are forced with .is-hover, .is-down and .is-focus; checked and disabled are real.
Aluminium
| Part | Empty | Filling | Focus | Complete | Checking | Right | Wrong | Not checked | Disabled |
|---|---|---|---|---|---|---|---|---|---|
| Digits | |||||||||
| Letters |
Graphite
| Part | Empty | Filling | Focus | Complete | Checking | Right | Wrong | Not checked | Disabled |
|---|---|---|---|---|---|---|---|---|---|
| Digits | |||||||||
| Letters |
| Name | Kind | Values | What it does |
|---|---|---|---|
| <tui-tumbler> | element | wraps one <input> | The lock: builds the slot, the wheels, the bar, the lamp and a polite live region around the input it is given. |
| <input> | element | name · id · value · required · disabled | The form control, kept real and laid over the wheels: it posts, and takes autocomplete="one-time-code", inputmode and maxlength from the part. |
| length | attribute | 4 · 6 · 8 (1–12), 6 | How many wheels. 6 and 8 are printed in two groups. |
| charset | attribute | "numeric" · "alphanumeric" | Digits, or digits and A–Z (typed letters are upper-cased; the phone shows its letter keyboard). |
| label | attribute | text | An aria-label for the input when no <label for> names it. |
| data-rest · data-short | attribute | "6 digits" · "Type all 6 digits." | The words beside the lamp at rest, and when Enter is pressed early. |
| data-checking · data-right · data-wrong · data-error | attribute | "Checking…" · "Code accepted" · … | The words for each outcome. A failed check adds its reason: “Could not check the code — meter not answering”. |
| data-state | attribute | empty · filling · complete · checking · right · wrong · error | Where it is. Written by the part; set it in markup only to show a state. |
| data-why | attribute | text | Why the check failed, from the rejection’s message. |
| .tui-tumbler--lg | class | — | 46px wheels, for a page whose one question is the code. |
| --tt-w · --tt-h · --tt-r · --tt-size | CSS property | 36px · 62px · 38px · 21px | A wheel’s width, the window’s height, the drum’s radius and the figures. Wheels narrow with their container, down to 24px. |
| .value · .input · .complete · .state | property | string · <input> · boolean · string | Read or set the code (setting turns the wheels and fires nothing); reach the input; every wheel set?; where it is. |
| .check(work) | method | promise, or function(code) → promise | Check on it: truthy is right, falsy is wrong. Resolves true or false; rejects with the error once the lock has printed it. One check at a time. |
| .reset() | method | — | Unlock and clear: the bar lifts and every wheel turns back to blank. |
| input · change | event | on the <input> | input on every change to the code; change when a turned wheel settles (and natively on blur). |
| complete | event | detail { value } | Every wheel is set. |
| verify | event | cancelable · detail { value, wait(promise) } | After typing, paste or autofill completes the code, and on Enter. Hand wait() the check; hand it nothing and Enter submits the form. |
| 0–9 · A–Z | keyboard | types | Fills the wheel at the cursor and moves on, overwriting as a lock does. Anything else is ignored. |
| Paste · autofill | keyboard | whole code | Fills every wheel from the start; a code inside a sentence (“Your code is 424242”) is found. |
| Backspace · Delete | keyboard | — | Clear back (or forward); the wheels after it move up. |
| ← → · Home · End | keyboard | the cursor | Move between wheels. The cursor’s wheel is marked with a printed bar while focused. |
| ↑ · ↓ | keyboard | ± one detent | Turn the cursor’s wheel. Turning never checks by itself; Enter does. |
| Enter | keyboard | check | Fires verify when every wheel is set; says what is missing otherwise. |
| Drag · wheel | pointer | 22px a detent · one notch | Drag a wheel up or down, or scroll over it while the lock has focus. A click puts the cursor on it. |
| .is-hover · .is-focus | class | on <tui-tumbler> | Forced states, for documentation and specimens only. |
<div class="tui-field">
<label class="tui-label" for="otp">Sign-in code</label>
<tui-tumbler length="6"
data-right="Signed in"
data-wrong="That code didn’t match. Check the latest email.">
<input id="otp" name="code">
</tui-tumbler>
</div>
<!-- A device PIN, and a recovery code with letters -->
<tui-tumbler length="4" label="Meter PIN"><input name="pin"></tui-tumbler>
<tui-tumbler length="8" charset="alphanumeric" label="Recovery code"><input name="recovery"></tui-tumbler>import '/kit/kit.mjs'; // defines <tui-tumbler>
const lock = document.querySelector('tui-tumbler');
// Every wheel set by typing, paste or autofill (or Enter): hand it the real check.
// Resolve true or false; reject only when the code could not be checked.
lock.addEventListener('verify', (e) => {
e.detail.wait(fetch('/auth/code', { method: 'POST', body: new URLSearchParams({ code: e.detail.value }) })
.then((r) => (r.ok ? true : r.status === 401 ? false : Promise.reject(new Error('Server busy, try again')))));
});
// Or check it yourself.
if (await lock.check(api.verify(lock.value))) location.assign('/runs');Needs kit.css and kit.mjs. How to install the kit.
autocomplete="one-time-code" field, and paste lands whole; split into six they fill the first box or nothing. A screen reader meets one edit field (“Sign-in code, 6 digits”) and hears each character as it is typed, instead of six stops.aria-hidden). ↑/↓ turning a wheel is an extra; typing does everything.aria-describedby: “Checking…”, “Code accepted”, “That code didn’t match. Type it again.” A wrong code sets aria-invalid until the next character.One custom element around a real input, which lies transparent over the wheels and takes every key, paste and autofill. Each wheel is an eleven-flat drum, a blank then 0–9 (letters ride the same drum past the window), drawn as five flats round the read line, each placed by its angle alone: translateY(sin a · r) scaleY(cos a), so the figures foreshorten as they roll over and under. The light is a separate fixed layer over the drum: occlusion under the slot’s walls, a band of the room’s brightness above the read line and the dark floor below it (chrome in aluminium, anodised black in graphite) so the figures pass through the reflection and it never turns with them. The slot’s walls are drawn over the wheels, so the upper wall shades their tops. The locking bar and its teeth rest on the rims; on a right code they drop 4px into notches and the drums settle 1.5px under them. A wrong code shakes the bank, then unwinds every wheel downward to blank, right to left.
One input under the wheels, so texts, authenticators and password managers can fill it. The server decides right and wrong; the lock only shows it.
5 rules in this file, with the foundations they rest on and the anti-patterns that break them. Full rules ship in the brain with the Pass.
Text entry in a recess, the label printed above it, and a hint that says how to fix an error.
Sign in and sign up on one plate: Google and GitHub keys with their marks stamped in monochrome, an email route underneath, a two-way selector that re-prints the plate, and errors that say how to fix them.
A key for work that takes time and must not run twice. It latches below the plate while the work runs, an LED matrix in its face scanning amber, and says how it ended.