Skip to the page
TACTILE UI

Kit part · Forms · Inputs · Sign in & sign up

Tumbler lock

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.

Element
<tui-tumbler> + <input>
Needs
kit.css + kit.mjs
Rules
tumbler.rules.md · 5
Finishes
Aluminium · Graphite · Porcelain · Black
Volumes
Tactile · Quiet
On this page
  1. Variants
  2. Every state, at once
  3. API
  4. Markup
  5. Accessibility
  6. How it’s built
  7. Rules
  8. Related parts
01

Variants

4 digits · a device PIN
6 digits · a sign-in code
8 characters · letters and digits
Large · the page’s one question
Right · the bar is down
Wrong · spun back to blank
Disabled · too many tries
02

Every state, at once

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

PartEmptyFillingFocusCompleteCheckingRightWrongNot checkedDisabled
Digits
Letters

Graphite

PartEmptyFillingFocusCompleteCheckingRightWrongNot checkedDisabled
Digits
Letters
03

API

NameKindValuesWhat it does
<tui-tumbler>elementwraps 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>elementname · id · value · required · disabledThe form control, kept real and laid over the wheels: it posts, and takes autocomplete="one-time-code", inputmode and maxlength from the part.
lengthattribute4 · 6 · 8 (1–12), 6How many wheels. 6 and 8 are printed in two groups.
charsetattribute"numeric" · "alphanumeric"Digits, or digits and A–Z (typed letters are upper-cased; the phone shows its letter keyboard).
labelattributetextAn aria-label for the input when no <label for> names it.
data-rest · data-shortattribute"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-errorattribute"Checking…" · "Code accepted" · …The words for each outcome. A failed check adds its reason: “Could not check the code — meter not answering”.
data-stateattributeempty · filling · complete · checking · right · wrong · errorWhere it is. Written by the part; set it in markup only to show a state.
data-whyattributetextWhy the check failed, from the rejection’s message.
.tui-tumbler--lgclass—46px wheels, for a page whose one question is the code.
--tt-w · --tt-h · --tt-r · --tt-sizeCSS property36px · 62px · 38px · 21pxA 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 · .statepropertystring · <input> · boolean · stringRead or set the code (setting turns the wheels and fires nothing); reach the input; every wheel set?; where it is.
.check(work)methodpromise, or function(code) → promiseCheck 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 · changeeventon the <input>input on every change to the code; change when a turned wheel settles (and natively on blur).
completeeventdetail { value }Every wheel is set.
verifyeventcancelable · 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–ZkeyboardtypesFills the wheel at the cursor and moves on, overwriting as a lock does. Anything else is ignored.
Paste · autofillkeyboardwhole codeFills every wheel from the start; a code inside a sentence (“Your code is 424242”) is found.
Backspace · Deletekeyboard—Clear back (or forward); the wheels after it move up.
← → · Home · Endkeyboardthe cursorMove between wheels. The cursor’s wheel is marked with a printed bar while focused.
↑ · ↓keyboard± one detentTurn the cursor’s wheel. Turning never checks by itself; Enter does.
EnterkeyboardcheckFires verify when every wheel is set; says what is missing otherwise.
Drag · wheelpointer22px a detent · one notchDrag a wheel up or down, or scroll over it while the lock has focus. A click puts the cursor on it.
.is-hover · .is-focusclasson <tui-tumbler>Forced states, for documentation and specimens only.
04

Markup

<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.

05

Accessibility

  • One labelled text input, not a spinbutton per wheel: SMS autofill, WebOTP and password managers fill a single 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.
  • The wheels, the bar and the notches are the input’s picture (aria-hidden). ↑/↓ turning a wheel is an extra; typing does everything.
  • The lamp always has words beside it, in a polite live region the input points to with aria-describedby: “Checking…”, “Code accepted”, “That code didn’t match. Type it again.” A wrong code sets aria-invalid until the next character.
  • While checking and once right the input is read-only, not disabled, so focus stays on it. A required lock that is not full says how many are missing as its validity message.
  • Reduced motion drops the rolling, the shake, the drop and the settle: the wheels jump to their characters, the bar is down or up, the lamp and the words carry every state.
06

How it’s built

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.

07

Rules

tumbler.rules.mdIn the brain

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.

08

Field

Text entry in a recess, the label printed above it, and a hint that says how to fix an error.

Sign-in

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.

Commit key

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.

Docs
Finish
Volume