Design Lounge
Install

Nº 483 of 520

Chitthi code entry

Six digit boxes for a masked Nepal mobile. Paste fills them, a wrong code shakes, and Resend waits 30 seconds.

Preview

Open full size
390 × 844 · 10.2 KB · 239 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Chitthi code entry" (piece phone-code-entry) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

241 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them. When a kit is locked, map colours onto the kit tokens. This is an iOS-style phone screen: a clear nav bar, a large title, 44px targets, and a swipe that can still go back because the page does not steal horizontal drags.

What it is

The one-time code screen of Chitthi, a fictional letter desk on New Road in Kathmandu. A code was just sent to a Nepal mobile, shown masked. Six boxes take one digit each. Typing advances. A paste fills the row. Only 482913 is accepted in this demo. Anything else shakes the row, prints the error under it, and clears the digits so the person can type again. Resend is already waiting when the screen appears, because the message was sent before this frame. The look is postal: cool grey ground, navy ink, a vermilion desk mark, Vollkorn for the title, Inconsolata for the digits.

Reference behaviour

  1. First frame. Title “Enter the code”. Dek “Sent to +977 98****418”, with the number in Inconsolata. Six boxes are empty. The first box is focused and shows a vermilion 3px foot. Resend reads “Resend in 30s” and aria-disabled="true". No error is visible. The success card is hidden.
  2. The countdown starts on load, not after an error. It steps once a second from 30 to 0. At 0 the label is “Resend code”, aria-disabled is false, and the status region says “You can resend the code.”
  3. A press while seconds remain does not send. Status says “Wait N seconds to resend.”
  4. A press at 0 sets status to “Code sent again.” and restarts 30 seconds. Digits already typed stay as they are.
  5. Typing a digit writes that digit, strips anything else, and moves focus to the next box. A non-digit key does not enter.
  6. Backspace on an empty box clears the previous box and moves focus there. Arrow Left and Arrow Right move focus without changing values.
  7. Paste anywhere in the row. Take the clipboard text, keep digits only, fill from box 1 up to six, and focus the last filled box. 482 913 and 48a2913 both become 482913.
  8. When six digits equal 482913, the row gains an ok foot in #1d6a42. Inputs disable. The title becomes “Desk open”. A card shows “Mina Gurung” and “Signed in at the New Road desk.” Resend hides. “Enter again” appears. Status says “Code accepted. The desk is open.” Focus moves to the title.
  9. When six digits are anything else, the row shakes for 420ms (translateX 0, -8px, 8px, -5px, 4px, 0) and gains the error stroke. The alert reads “That code does not match. Try again.” Each box gets aria-invalid="true".
  10. After 420ms the six values clear, focus returns to box 1, and the shake class leaves. The error text stays until the next digit or paste.
  11. The next digit or paste clears the error and aria-invalid before writing.
  12. “Enter again” restores the first frame’s copy and empty boxes, focuses box 1, and shows Resend again. It does not reset the countdown unless a resend press does.
  13. Reduced motion: the shake animation lasts 1ms. The digits still clear after 420ms. The error still appears.

Structure

390 x 844
padding inline 16px, top safe 54px, bottom safe 34px
+--------------------------------------+
| Chitthi                    [.] DESK 04|  nav, 1px line under, 10px pad below
| Enter the code                       |  34px Vollkorn
| Sent to +977 98****418               |  17px, number in Inconsolata
| [ ] [ ] [ ] [ ] [ ] [ ]              |  6 columns, gap 8px, min-height 56px
| That code does not match. Try again. |  alert, hidden until a miss
| Resend in 30s                        |  44px text button, left
|                                      |
| Do not share this code.              |  foot, Inconsolata 13px, pinned low
+--------------------------------------+

Success, same screen:
| Desk open                            |
| Sent to +977 98****418               |
| [4] [8] [2] [9] [1] [3]              |  ok foot, inputs disabled
| +----------------------------------+ |
| | Mina Gurung                      | |
| | Signed in at the New Road desk.  | |
| +----------------------------------+ |
| Enter again                          |
  • The nav brand and the desk label are paragraphs, margin 0. The vermilion swatch is an 8px square, aria-hidden.
  • The six inputs sit in role="group", labelled by the h1, described by the error.
  • Boxes are inputmode="numeric", maxlength="1", pattern="[0-9]*". Only the first has autocomplete="one-time-code" and autofocus.
  • Each box has an accessible name “Digit 1” through “Digit 6”.
  • The error is role="alert". While it has no text it is display: none.
  • The success card and “Enter again” use the hidden attribute until the code matches.
  • A visually hidden role="status" speaks resend and success. It does not speak each second.
  • main is a column with flex: 1. The foot uses margin-top: auto so it sits on the bottom safe area while the boxes stay under the title.

Tokens

:root {
  --bg: #e6e8ef;
  --surface: #f4f6fa;
  --ink: #172033;
  --ink-2: #3c4760;
  --ink-3: #515a70;
  --line: #cfd4e0;
  --accent: #9e2b22;
  --ok: #1d6a42;
  --error: #9e2b22;
  --focus: #172033;
  --serif: "Vollkorn", Georgia, serif;
  --mono: "Inconsolata", ui-monospace, monospace;
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --fast: 160ms;
  --mid: 240ms;
  --shake: 420ms;
  --radius: 10px;
}

The vermilion is the desk mark, the focused foot, the error, and the ready Resend label. It is not a full-width button fill. Success uses --ok, not vermilion.

Typography

RoleFamilySizeWeightLine heightTrackingCase
WordmarkVollkorn21.6px6001-0.02emsentence
Desk labelInconsolata12px60010.12emupper
Large titleVollkorn34px6001.12-0.02emsentence
DekVollkorn17px4001.40sentence
Masked numberInconsolata16px60010.02emas written
DigitInconsolata28px60010numeric
ErrorVollkorn15px6001.350sentence
Resend, Enter againInconsolata16px60010sentence
Card nameVollkorn18px6001.30sentence
Card meta, footVollkorn meta 15px, Inconsolata foot 13px4001.40sentence

Digits are tabular in the sense that Inconsolata is a mono. Set font-variant-numeric: tabular-nums on Resend so 30 and 9 do not jog the line. Do not set the digits in Vollkorn.

Motion

ThingTriggerPropertyFromToDurationEasingReduced motion
Wrong codesix wrong digitstranslateX0-8, 8, -5, 4, 0420mscubic-bezier(0.2, 0.7, 0.2, 1)1ms, digits still clear at 420ms
Resendeach secondtext“Resend in 30s”“Resend code”1000ms stepnonecountdown still runs

The shake keyframes are 0% and 100% at 0, 20% at -8px, 40% at 8px, 60% at -5px, 80% at 4px. Remove the shake class on animationend so a second miss can replay it. Force reflow (offsetWidth) before adding the class again.

States

  • Box empty: 1.5px navy border, radius 10px, surface fill, min-height 56px.
  • Box focused, and the first box before anyone tabs: vermilion border and box-shadow: inset 0 -3px 0 var(--accent). No second outline, so the box does not look double. Buttons keep the 2px navy outline.
  • Box filled: the digit is ink, 28px, weight 600.
  • Row wrong: every border --error, no foot shadow, alert under the row.
  • Row accepted: every border --ok, foot --ok, inputs disabled.
  • Resend waiting: colour --ink-3, aria-disabled="true".
  • Resend ready: colour --accent, aria-disabled="false".
  • Success card: surface, 1px --line, radius 12px, padding 14px 16px.
  • Enter again: 44px tall, ink, Inconsolata.

Accessibility

  • The group is named by the title. Boxes are named Digit 1 to Digit 6. Do not rely on placeholder dots.
  • aria-invalid is set on each box only while the error is showing. Clearing the digits does not remove the alert text. The next input does.
  • role="alert" on the error. role="status" for resend and success only.
  • Keyboard: digits, Backspace, Arrow Left, Arrow Right, paste. A one-character non-digit is prevented on keydown.
  • Hit targets: each box is at least 44px tall (56px at the default root). Resend and Enter again are at least 44px tall. The nav row is at least 44px under the safe area.
  • Contrast: #172033 on #f4f6fa clears 4.5:1. #515a70 on #e6e8ef clears 4.5:1. #9e2b22 on #f4f6fa clears 4.5:1. #1d6a42 on #f4f6fa clears 4.5:1.
  • Do not move focus in a loop. After a miss, focus box 1. After success, focus the title (tabindex="-1").

Responsive rules

  • The frame is 390 by 844. Inline padding 16px. Top max(54px, env(safe-area-inset-top)). Bottom max(34px, env(safe-area-inset-bottom)).
  • The boxes are grid-template-columns: repeat(6, minmax(0, 1fr)) with gap 8px. They shrink. They do not overflow sideways at 360 wide. At 360 the cell is about 48px, still at least 44px tall.
  • At a 32px root, the title and dek wrap, Resend grows with the font, and the boxes stay on one row because the grid is fractional. Digit size is 1.75rem, so the box min-height: 3.5rem grows with the text and the page scrolls vertically. There is no two-column text row on this screen. The nav’s two ends wrap if the wordmark and the desk label no longer fit.
  • touch-action: pan-y on the body so a horizontal swipe is not captured by the boxes.
  • At tablet width, centre a 390px column. Do not draw six boxes across 1180px.
  • Do not draw a status bar or a home indicator.

Acceptance checklist

Always

  • Six single-digit boxes. Typing a digit advances. Backspace on an empty box goes back.
  • Paste fills from the first box with digits only, up to six.
  • A wrong code shakes the row, shows an error under it, then clears the digits and keeps the error until the next entry.
  • Resend waits 30 seconds from the moment this screen is shown, then waits 30 seconds again after each resend.
  • The destination is masked and is not a US 555 number.
  • The first frame is empty, with the first box focused.
  • Targets are at least 44px. Safe areas are 54px top and 34px bottom.
  • Reduced motion shortens the shake to 1ms and still clears the digits.

This demo

  • Brand is Chitthi. Nav meta is “DESK 04”. Title starts as “Enter the code”.
  • The masked destination is +977 98****418.
  • The only accepted code is 482913. Any other six digits use the error “That code does not match. Try again.”
  • Success title is “Desk open”. The card reads “Mina Gurung” and “Signed in at the New Road desk.”
  • Resend waiting copy is “Resend in 30s”. Ready copy is “Resend code”. Status on a resend is “Code sent again.”
  • Foot copy is “Do not share this code.”
  • Ground #e6e8ef, surface #f4f6fa, ink #172033, vermilion #9e2b22, ok #1d6a42.
  • Fonts are Vollkorn and Inconsolata. Box radius is 10px. Shake is 420ms.

Implementation notes

Keep the boxes as six inputs so paste, focus, and labels stay native. Reject non-digits and advance:

box.addEventListener('input', function () {
  if (lock || done) return;
  box.value = box.value.replace(/\D/g, '').slice(-1);
  if (box.value && i < 5) boxes[i + 1].focus();
  if (code().length === 6) judge();
});

Paste must preventDefault, or the browser will also drop the whole string into one box:

box.addEventListener('paste', function (e) {
  e.preventDefault();
  var text = (e.clipboardData && e.clipboardData.getData('text')) || '';
  fill(text.replace(/\D/g, '').slice(0, 6));
});

A miss sets a lock flag so a second input event cannot judge again before the clear:

lock = true;
row.classList.remove('shake');
void row.offsetWidth;
row.classList.add('shake', 'bad');
setTimeout(function () {
  boxes.forEach(function (b) { b.value = ''; });
  boxes[0].focus();
  lock = false;
}, 420);

Common mistakes:

  • One hidden input and six decorative spans, then paste and screen readers both fail.
  • Clearing the error in the same timeout as the digits, so nobody can read it.
  • Accepting any six digits. This demo accepts only 482913.
  • A US number such as +1 555 0100. The mask is a Nepal mobile, country code 977, prefix 98.
  • Starting Resend only after a failure. The wait is already running on the first frame.
  • Circular iOS passcode dots. These are rectangular boxes with a vermilion foot.
  • Drawing the status bar or a home indicator.
  • Announcing the countdown every second.

Where it sits:

  1. It follows a “send a code” step that is not in this piece. That is why the number is already masked and the timer is already running.
  2. Success means the desk account is open. The card is that consequence. It is not a second app home.
  3. “Enter again” is the gallery replay. A product would leave this screen instead.

Rebuild order:

  1. Nav with Chitthi and DESK 04, 54px top, 1px line.
  2. Title, masked number, six-column boxes, first box focused.
  3. Wire typing, arrows, backspace, and paste.
  4. Accept only 482913. Shake, error, clear digits after 420ms.
  5. Show the Mina Gurung card on success.
  6. Start the 30 second resend on load.
  7. Pin the foot, then add reduced motion.
  8. Name the boxes Digit 1 through Digit 6. The masked number stays in the dek, not inside a box.
  9. Keep the vermilion off the full-width controls. It is the desk swatch, the focused foot, the error, and the ready Resend label.
  10. The success card is one account, Mina Gurung at the New Road desk. Do not add a second row to match the lock piece.
  11. Box gap is 8px. At 390px with 16px of inline padding the six cells share the leftover width. None of them uses a fixed width that could overflow.

Details

Palette
Type
Vollkorn · Inconsolata
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Igloo

A product with one job. Do not invent a homepage they did not ship.

All sources