<!-- Design Lounge Nº 322 · "One-time code" · designlounge.vercel.app -->

# One-time code

> **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, each cell uses `--control` width at least 40px and the family's radius. A password is `password-field`. A link in an email is `auth-magic-link-sent`.

## What it is

Six cells for a code from a letter. The label is "Code from the letter." The first three cells are 1, 8, and 4. The last three are empty. The status line reads "3 of 6." A digit moves focus to the next cell. Backspace on an empty cell moves to the previous. Paste of digits fills from the start. Check the code stays disabled until six digits. The code that passes is 184219. Any other six digits turns the borders danger and shows "The code is 184219." A pass writes "Code accepted." and disables the button. Check is the one solid primary.

## Reference behaviour

1. Cells 1, 8, 4 are filled. Cells 4, 5, and 6 are empty. The status line is "3 of 6." Check is disabled.
2. Typing a digit keeps one character, strips anything that is not a digit, and focuses the next cell.
3. Backspace on an empty cell focuses the previous cell.
4. Pasting 184219 fills all six and enables Check.
5. Check with 184219 hides the error, writes "Code accepted.", and disables the button.
6. Check with any other six digits shows the error and sets `aria-invalid` on the cells.
7. There is no animation. Focus ring is 2px `--focus`, offset 2px on a cell and offset 3px on the button.

## Structure

```
padding 48px 64px
Sign in                      12px
Code from the letter         12px label
[ 1 ] [ 8 ] [ 4 ] [ ] [ ] [ ]    each 40 by 48
[ Check the code ]           40px, disabled until six
3 of 6.                      or the error, or Code accepted.
```

- The six inputs are a group labelled by the sentence.
- Each input has its own name: Digit 1 through Digit 6.
- The first input may use `autocomplete="one-time-code"`.

## Tokens

```css
:root {
  --bg: #f6f4ef;
  --surface: #ffffff;
  --ink: #161513;
  --ink-2: #5a554c;
  --line-strong: #cfc6b8;
  --primary: #1f4d3a;
  --primary-ink: #fffdf8;
  --danger: #9b2c2c;
  --focus: #1f4d3a;
  --sans: "IBM Plex Sans", system-ui, sans-serif;
}
```

Cell radius is 2px. The family replaces it. Do not join the six cells into one rounded track unless the family is already a pill, and even then keep a gap of 8px so they stay six targets.

## Typography

| Role | Family | Size | Weight | Colour |
| --- | --- | --- | --- | --- |
| Where | sans | 12px | 500 | `--ink-2` |
| Label | sans | 12px | 500 | `--ink-2` |
| Digit | sans | 20px | 500 | `--ink` |
| Button | sans | 13px | 500 | `--primary-ink` |
| Status | sans | 14px | 400 | `--ink` |
| Error | sans | 14px | 400 | `--danger` |

Digits are centered and tabular. The where-line letter-spacing is 0.04em.

## Motion

None. Focus moves in one frame. Reduced motion has nothing to remove. Do not flip the cells.

| Thing | Trigger | What changes |
| --- | --- | --- |
| Digit | input | the cell, focus moves forward |
| Backspace | empty cell | focus moves back |
| Paste | digits | the cells from the start |
| Check | six digits | accepted, or the error |

## States

- Cell resting: 40px wide, 48px tall, radius 2px, 1px `--line-strong`, surface, text centered.
- Cell invalid: border `--danger`, only after a failed check.
- Check disabled: opacity 0.4, until six digits, and again after a pass.
- Check enabled: solid `--primary`, text `--primary-ink`.
- Status: "n of 6." while the code is short. Empty while a result is showing.
- Error: "The code is 184219." This demo names the code so the piece can be tested. A product says the code was not the one in the letter, and does not print the real code.
- Focus-visible: 2px outline, offset 2px on a cell.

## Accessibility

- The group is labelled "Code from the letter."
- Each cell has its own name, Digit 1 through Digit 6.
- `inputmode="numeric"` opens a digit keyboard.
- A failed check sets `aria-invalid` on the cells. The error is text under them.
- Check is disabled until the code is complete, so an empty submit cannot happen.
- Hit targets are at least 40 by 48. On a phone, at least 44px.
- Contrast of the digits on white and the error on the paper clears 4.5.
- Do not use one input and pretend it is six. The caret and the paste behaviour depend on the cells.

## Responsive rules

- At 1280 the row is six cells with an 8px gap, padding 48px 64px.
- Below 640 the cells stay 40px and the gap may drop to 6px. They stay on one row. Do not wrap a code.
- On a phone the cell height is at least 44px.
- Do not replace the cells with a password field.

## Acceptance checklist

- [ ] Six cells. The first three are 1, 8, 4. The status line is "3 of 6."
- [ ] Check is disabled until six digits.
- [ ] A digit focuses the next cell. Backspace on an empty cell focuses the previous.
- [ ] Paste fills from the first cell.
- [ ] 184219 is accepted. The status line is "Code accepted."
- [ ] Another six-digit code shows "The code is 184219." and danger borders.
- [ ] Cells are 40 by 48, radius 2px, digit 20px.
- [ ] Check is the only solid button.
- [ ] Focus ring is 2px.
- [ ] There is no animation.

## Implementation notes

Keep one character. Move focus yourself.

```js
box.value = box.value.replace(/\D/g, '').slice(-1);
if (box.value && i < 5) boxes[i + 1].focus();
```

Paste reads text, keeps digits, and fills from the first cell.

Common mistakes:

- One long input. The person cannot see which digit is missing.
- Accepting letters.
- Enabling Check at three digits.
- Printing the real code in a product error. This demo prints 184219 so the test is visible. A product does not.
- A second primary beside Check.
- Cells shorter than 40px.
- A countdown that resends on its own. A resend is a separate outline button, not in this piece.

Where it sits in a product:

1. Use it after a letter or a message has sent a code. The sent state is `auth-magic-link-sent` when the method is a link.
2. A password the person chose is `password-field`.
3. Check is the one primary.
4. Cell size and radius follow the family, and the cell stays at least 40px.
5. When a theme is locked, Check uses `--primary` and `--primary-ink`.
6. Six digits is this letter. A product of four digits uses four cells and says so in the count.
7. The where-line Sign in is the screen name.
8. Do not also ask for the password on the same view unless the product truly needs both, and then the password is the other control.
9. The opening digits 1, 8, 4 match the start of 184219 so a tester can finish the code.
10. Keep the credit line on the token block.

Rebuild order:

1. Set the paper and IBM Plex Sans.
2. Place the label and six cells, with 1, 8, 4 filled.
3. Place Check, disabled, and the status line.
4. Wire input, backspace, and paste.
5. Wire Check against 184219.
6. Map the button and the radius onto the kit.

Copy you keep:

1. Sign in.
2. Code from the letter.
3. Digit 1 through Digit 6.
4. 3 of 6. as the start.
5. Check the code.
6. The code is 184219.
7. Code accepted.
8. The passing code 184219.

---

*From Design Lounge (https://designlounge.vercel.app). Live demo and source: /p/otp-code. Free to use in your products. Credit line: Designed using Design Lounge.*
