⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Cell Input

Fixed-shape entry, one cell per character

Website / Cell Input
Download .md file
Open .md in new tab
On this page
  • Anatomy
  • Basic usage
  • Format
  • Masking
  • Verdicts
  • Static display
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product

A cell input renders a fixed-shape entry — a verification code, a PIN, a date, a sort code, a quiz answer — as one cell per character, so the reader sees the shape before typing a character. One real <input> carries focus, value and the accessible name; the cells are decoration that mirrors it, and cell-input.js builds and paints them from the data-format you declare. The class is required; data-format declares the shape.

On a phone this matters: the single input keeps the OS caret, the soft keyboard and SMS autofill working where the reader is looking. Do not split it into one input per cell.


Anatomy

<div class="cell-input" data-format="6" data-mode="digits">
  <label class="cell-input-cells" for="code" aria-hidden="true"></label>
  <input type="text" id="code" aria-label="Verification code: 6 digits"
         autocomplete="one-time-code" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
  • — the block. Declares data-format and the optional axes below.
  • — a <label> for the input, aria-hidden="true". The script builds groups of spans into it: data-cell="letter" cells take typed characters, data-cell="separator" cells show pre-revealed punctuation. Tapping any cell focuses the input through the label.
  • The <input> — the only focusable, accessible control. It sits invisibly over the block, and its value is always the clean, clamped entry.
Attribute On Values What it does
data-format block "6", "3 4", "2-2-2", "2/2/4" Cell structure — see Format
data-mode block letters, digits, any (default) Filters input; digits sets the numeric keypad
data-mask block boolean Cells render dots instead of characters
data-size block small Compact cells for long formats
data-type block success, danger Verdict colouring — see Verdicts
data-static block boolean Display-only cells, no input, no JS

The component owns --cell-input-* geometry and colour tokens; override those rather than the selectors.

Basic usage

<div class="cell-input" data-format="6" data-mode="digits">
  <label class="cell-input-cells" for="code" aria-hidden="true"></label>
  <input type="text" id="code" aria-label="Verification code: 6 digits"
         autocomplete="one-time-code" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>

Typing fills the cells left to right. While the input has focus, the next empty cell carries a 2px accent border — it is both the caret and the focus indicator, the same pattern platform code inputs use. Paste works and keeps every legal character: pasting 20-41-53 into a sort code drops the hyphens and lands six digits. On a 6-digit code, autocomplete="one-time-code" lets iOS offer the SMS code above the keyboard — a single real input makes that work; one input per cell breaks it.

Format

data-format describes the shape. Digits declare a run of typeable cells; everything else is structure:

Character Meaning
6, 12, … That many typeable cells
space Word break — words wrap as units
- or / A shown separator cell, plus a wrap point
anything else (', .) A shown separator inside the word

Separators are pre-revealed punctuation: they are displayed, never typed, and the filter skips them on paste.

A two-word answer — the quiz's original use:

<div class="cell-input" data-format="6 4" data-mode="letters">…</div>

A date, day / month / year:

<div class="cell-input" data-format="2/2/4" data-mode="digits">…</div>

A bank sort code:

<div class="cell-input" data-format="2-2-2" data-mode="digits">…</div>

A licence key — long formats take data-size="small":

<div class="cell-input" data-format="4-4-4" data-size="small">…</div>

Cells uppercase their display with CSS; the stored value keeps the case that was typed. Say so near the field if the code is case-sensitive.

Masking

data-mask renders filled cells as dots. The masking is visual only — the real value stays in the input, and a screen reader echoes the real characters as they are typed. For a genuine secret, make the input type="password": the cells still paint, and assistive tech treats the value as protected. Reserve data-mask alone for shoulder-surfing theatre — a quiz answer, not a bank PIN.

<div class="cell-input" data-format="4" data-mode="digits" data-mask>
  <label class="cell-input-cells" for="pin" aria-hidden="true"></label>
  <input type="password" id="pin" aria-label="PIN: 4 digits" autocomplete="off" spellcheck="false">
</div>

Verdicts

After judging an entry, set data-type="success" or data-type="danger" on the block — letter cells take the status colours, separators stay quiet. on the block paints the danger state for live validation.

Colour is never the whole message. A verdict ships with a visible text outcome in a role="status" region, and danger sets aria-invalid="true" on the input — without those, the state change is invisible to a screen reader and ambiguous to colour-blind readers.

gaggle

Correct — that's the answer.

don't

Not this time — check the spelling and try again.

<div class="cell-input" data-format="6" data-mode="digits" data-type="danger">
  <label class="cell-input-cells" for="code" aria-hidden="true"></label>
  <input type="text" id="code" aria-label="Verification code: 6 digits" aria-invalid="true">
</div>
<p role="status">That code has expired — a new one is on its way.</p>

Static display

Cells with no input and no script — for showing a code rather than collecting one: a booking reference on a confirmation page, an example in documentation. Hand-author the cells with their characters, add data-static, and leave aria-hidden off so the code stays readable to everyone.

bd-7724
<div class="cell-input" data-static>
  <span class="cell-input-cells"><span class="cell-input-word"><span class="cell-input-cell" data-cell="letter">b</span><span class="cell-input-cell" data-cell="letter">d</span><span class="cell-input-cell" data-cell="separator">-</span></span><span class="cell-input-word"><span class="cell-input-cell" data-cell="letter">7</span><span class="cell-input-cell" data-cell="letter">7</span><span class="cell-input-cell" data-cell="letter">2</span><span class="cell-input-cell" data-cell="letter">4</span></span></span>
</div>

This variant is HTML and CSS only — it works without cell-input.js.

JavaScript

Include assets/js/cell-input.js. It binds every .cell-input[data-format] that contains an input and a element, on page load and again after every Barba navigation; a data-cell-input-bound guard makes re-runs harmless. No GSAP, no dependencies.

The script builds the cells, enforces aria-hidden on the cells row, filters and clamps the value on every input event (accents folded, illegal characters dropped, pasted separators skipped), and dispatches two bubbling events on the block:

Event detail When
cell-input:change { value, complete } The clean value changed
cell-input:complete { value } The final cell filled
document.getElementById('otp-block').addEventListener('cell-input:complete', function (event) {
  submitCode(event.detail.value);
});

Two contracts worth knowing:

  • Format and wiring are read once at bind time. To change data-format or swap the input afterwards, clear data-cell-input-bound and call window.initCellInput(block.parentNode).
  • Setting input.value from script fires no event; dispatch an input or change event after it and the cells repaint.

A data-format with no typeable cells is an authoring error: the script leaves the block inert and names it in a console warning.

Accessibility

  • The input must carry the accessible name. Write an aria-label (or aria-labelledby) that names the purpose and the shape: "Verification code: 6 digits". The generated fallback describes shape only — do not ship it.
  • Keep the cells row aria-hidden="true" whenever the block contains an input — the script enforces it. The cells are decoration; the input is the control.
  • Put a visible label or instruction above the block. The invisible name is for assistive tech, not a substitute for telling everyone what goes here.
  • Verdict states must pair colour with a visible text outcome in a role="status" region, and danger must set aria-invalid="true" on the input.
  • Real secrets must use type="password"; data-mask alone leaves the value exposed to character echo and password managers.
  • Static displays must not be aria-hidden — the characters are the content.
  • Disabling means both disabled on the input and on the block.
  • Keep the caret affordance — the 2px accent border on the next empty cell is the focus indicator. Override it and a keyboard user cannot see focus at all, because the input itself is invisible.

Usage rules

Do:

  • Use it when the entry has a fixed, known shape — codes, PINs, dates, sort codes, quiz answers
  • Match data-format to the shape the reader already knows — a sort code is 2-2-2, never 6
  • Wire cell-input:complete for auto-advance flows instead of watching keystrokes
  • Use data-size="small" when a format runs past roughly 16 cells

Don't:

  • Use it for free-form text — a name or an email is a plain input
  • Build one input per cell — focus juggling breaks paste, autofill and screen readers
  • Reveal a secret's length when the length itself is sensitive; use a plain type="password" input instead
  • Apply data-type without a text verdict alongside

CSS reference

This section documents how the component is built. For usage, see the sections above.

Tokens

Token Default What it controls
2rem Letter cell width
2.5rem Cell height
1.25rem Separator cell width
Gap between cells in a word
Gap between words
Cell fill
Cell border
Cell and ring radius

Selectors

Selector Purpose
Block: positioning context for the overlaid input
.cell-input input The real control, stretched invisibly over the cells
The label row; tapping it focuses the input
A word group; wraps as a unit
One cell; data-cell="letter" or data-cell="separator"
.cell-input-cell.is-active The caret cell — the focus indicator, visible only while focused
.cell-input-cell.is-filled A cell holding a character (JS hook, unstyled)
.cell-input[data-size="small"] Re-points the geometry tokens to compact values
.cell-input[data-type="success"] / [data-type="danger"] Verdict colours on letter cells
.cell-input.is-error Paints the danger state
.cell-input.is-disabled Disabled surface; pair with disabled on the input
.cell-input[data-static] Display-only: default cursor, hand-authored cells

Key rules

Overlay: the input is position: absolute; inset: 0; opacity: 0; pointer-events: none — the OS caret geometry lands on the cells, and the label forwards taps to the input.
Focus: the caret cell is the focus indicator — border-color: var(--input-focus) plus a 1px inset shadow for 2px of visual weight, gated on :focus-within. No ring on the group: the caret says "focused, and here". A forced-colors block outlines the row instead, since high-contrast mode erases the caret's colour.
Wrapping: row and word are both flex-wrap: wrap — words wrap as units, an over-long word wraps within itself.
Motion: cell border and background transition on --duration-xs / --ease-out, removed under prefers-reduced-motion: reduce.


Use in another product

The design system installs once per product:

npm install github:bydefaultstudio/design-system-dist#semver:^4.7.0

This component's styles ship in design-system.css. Its behaviour ships as dist/js/cell-input.js — copy it into the product's served assets and include it once per page:

<script src="assets/js/cell-input.js" defer></script>

In React, render this contract through the packaged adapter instead of writing the markup by hand:

import { CellInput } from '@bydefaultstudio/design-system/react';

The adapter renders the contract above and bridges this component's events to props — see React.

On this page
  • Anatomy
  • Basic usage
  • Format
  • Masking
  • Verdicts
  • Static display
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product
Previous Login
Next Skeleton

Was this page helpful?

We use this feedback to improve our documentation.

Thanks for your feedback

Send feedback

© 2026 By Default