Six boxes in two groups of three that act as one field, for a code sent by email or text message.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
errorsmbk-one-time-code-input--error | On or off | Off | The code is wrong or has run out. Draws the danger edge on every cell, kept while focused; the control also carries aria-invalid. |
Examples
Usage
In a field with its label and a help line saying where the code went: three digits typed and the fourth cell waiting.
Empty
Nothing typed yet. With focus, the first cell takes the focus edge.
Complete
All six digits in.
Error
A wrong code: every cell draws the danger edge, and the field says what to do.
Disabled
Disabled, as after too many tries.
Guidelines
Purpose
A field for a short code sent by email or text message to prove who someone is: six boxes in two groups of three, one digit in each, so a reader can check the code against the message at a glance. It acts as one field: one value, one name, one place the phone's suggested code goes. For any other short value, such as a postcode or a PIN a person chose themselves, use a plain input.
When to use
- Checking a sign-in, a new email address or a payment with a code the product has just sent.
- Any code of exactly six digits that a person copies from a message.
When not to use
- A code with letters in it, or of another length. Use an input.
- A password or a PIN the person chose. Use the password field.
- A number someone works out or adjusts. Use the number field.
Usage rules
Do
- Put it in a field with a label and a help line saying where the code went.
- Offer a way to get a new code beside it.
- Send the code with
onCompleteonly where a wrong code costs nothing; otherwise let the reader press a button.
Don't
- Use it for a value of any other length or with letters.
- Split the code into six separate fields of your own: a paste, the phone's suggestion and a screen reader all expect one.
Say where the code went and how long it lasts, under a label that names it.
Cells with no label or help leave the reader guessing which code to type, and a screen reader with no name to say.
Anatomy
- The block, holding the two groups and the control.
- The group part, three cells side by side; a wider gap between the two groups splits the code in half, as it is read.
- The cell part, one box: the input's well at the large control's height, square, with the large control's corners. Its markup carries the input's classes.
- The digit part, the digit a cell shows.
- The control part, one native text control laid unseen over every cell. It holds the code; the cells only show it.
Behavior
Typing a digit fills the active cell and moves to the next. Backspace clears the last digit and goes back a cell, so pressing it again and again empties the cells from the end; on a filled cell picked in the middle it clears that cell's digit, and the digits after it close up, since the code is one value with no gaps. The arrow keys, Home and End move between the cells, and a press picks the cell under it, never one past the next empty cell. On a filled cell, typing replaces its digit. A paste fills every cell from the start with the digits it holds, whatever else it holds, such as spaces or a dash. Anything but a digit is kept out.
The cell where the next digit goes takes the focus edge while the field has focus. In error every cell draws the danger edge, kept while focused. Disabled and read-only cells take the input's disabled and read-only looks.
On a phone the keyboard shows digits, and the phone can offer the code from a message it has just received, which fills every cell. In a space narrower than the six cells, they narrow together and keep their height, so the page never scrolls sideways.
The value is one string of digits. The React component reports it with onChange each time it changes, and the whole code with onComplete once the sixth digit is in. In a form, the control's name sends the code as one value.
Accessibility
What it does for you
- It is one native text field, so a screen reader announces one field by its name, with the digits typed so far as its value, and a form, a password manager and the phone's code suggestion all see one field.
- The cells are hidden from assistive technology, since the field already holds their digits.
- Inside a field component it takes the field's label, help and error lines, its required, disabled and read-only states, and marks itself invalid in error.
- It asks for the numeric keyboard and says it takes a one-time code (
autocomplete="one-time-code"), so the phone can offer the code.
What you must do
- Give it a name: put it in a field with a label such as "Verification code", or give it an
aria-label. - Say under it where the code was sent and how long it lasts, in the field's help line, and how to get a new one.
- Never send the code away, or clear the cells, on a timer the reader cannot stop. If the code runs out, say so in the field's error line and offer a new one.
- In markup written by hand, write the cells with the input's classes at the large size, hide the groups from assistive technology, mark the cell where the next digit goes
data-active, and write the error modifier on each cell in error. The cells only show the digits; keeping them in step with the control needs script.
How to test
- Tab to the field: the first empty cell takes the focus edge.
- Type six digits: each fills a cell and moves on. Press Backspace: the last digit clears. Paste a code with a space in it: every cell fills.
- With a screen reader, the field is read once, by its label, with its digits as its value and its help line after.
- On a phone, the keyboard shows digits.
- With the page 320 pixels wide, the cells narrow and nothing scrolls sideways.
Keyboard
| Key | What it does |
|---|---|
| 0 to 9 | Fills the active cell and moves to the next. |
| Backspace | Clears the last digit and goes back a cell; on a filled cell picked in the middle, clears its digit. |
| Left arroworRight arrow | Moves to the cell before or after, never past the next empty cell. |
| HomeorEnd | Moves to the first cell, or to the cell after the last digit. |
Source
The files the one-time code input is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-one-time-code-input — One-time code input. Figma "One-time code input" 8217:3276 (State: Empty, Focused,
* Partly filled, Complete, Error, Disabled).
* Six cells in two groups of three that act as one field. Each cell is the input's well at the large control's
* height, square, with the large control's corners: its markup carries smbk-input and smbk-input--size-lg, so the
* input's stylesheet draws the well, its error edge and its disabled and read-only looks, and names the cell marked
* data-active, which takes the focus edge while the control has focus. One native text control lies over every cell,
* unseen, and holds the value: what is typed, pasted or filled in by the phone goes there, and the cells show it.
* This stylesheet only lays the cells out and lays the control over them. */
.smbk-one-time-code-input {
position: relative;
display: inline-flex;
gap: var(--smbk-space-24);
max-width: 100%;
vertical-align: top;
}
.smbk-one-time-code-input__group {
display: flex;
gap: var(--smbk-space-inline-md);
min-width: 0;
}
/* A cell is the large well made square, its digit centred. In a space narrower than the six, the cells narrow
together and keep their height, so the page never scrolls sideways. Two classes, so the input's own size rules,
whatever order the stylesheets load in, never widen or pad it. */
.smbk-input.smbk-one-time-code-input__cell {
flex: 0 1 auto;
width: var(--smbk-size-control-lg);
min-width: 0;
padding: 0;
justify-content: center;
}
.smbk-one-time-code-input__digit {
font-variant-numeric: tabular-nums;
}
/* The control lies over every cell and takes every press, unseen: the cells draw its value, so its own text, caret,
selection and the browser's fill colour are never shown. Its text is the cells' own size, so a phone's browser
never zooms in as it takes focus. */
.smbk-one-time-code-input__control {
position: absolute;
inset: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 0;
opacity: 0;
font-family: var(--smbk-typography-body-md-font-family);
font-size: var(--smbk-typography-body-md-font-size);
cursor: text;
}
.smbk-one-time-code-input__control:disabled {
cursor: not-allowed;
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.24--smbk-space-24 | 24px | Spacing | |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| size.control.lg--smbk-size-control-lg | comfortable: 48pxcompact: 40pxchanges with density | Size | |
| typography.body-md--smbk-typography-body-md-font-family--smbk-typography-body-md-font-size--smbk-typography-body-md-font-weight--smbk-typography-body-md-line-height--smbk-typography-body-md-letter-spacing--smbk-typography-body-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
Manifest
{
"name": "one-time-code-input",
"block": "smbk-one-time-code-input",
"summary": "Six boxes in two groups of three that act as one field, for a code sent by email or text message.",
"adapt": "css",
"axes": {},
"flags": {
"error": {
"description": "The code is wrong or has run out. Draws the danger edge on every cell, kept while focused; the control also carries aria-invalid."
}
},
"parts": [
"group",
"cell",
"digit",
"control"
],
"figmaNodeId": "8217:3276"
}