InputOTP

A segmented one-time-code field with sequential entry, backspace deletion, and paste distribution.

Overview

InputOTP is a segmented field for one-time codes — a row of single-character cells backed by one left-filled string value. Use it for the verification codes sent by email or SMS and for authenticator (TOTP) entry. Typing advances through the cells, Backspace deletes the last character, and pasting a code distributes it across the cells.

Anatomy

A labeled role="group" wrapping length cells. Each cell is a bare, single-character <input> inside its own sketchy SketchSurface box: the active cell shows the focus stroke, filled cells a stronger stroke, and an invalid field paints every cell in the danger stroke. Every color, size, and sketch parameter comes from @ghds/tokens (comp.inputOtp.*).

Variants & States

No named variants — instead length, mode, and mask shape the field, and disabled / invalid set its state. Cells also react to being active (focused) and filled.

State Status Notes
Active Implemented The focused cell shows comp.inputOtp.stroke.active.
Filled Implemented Filled cells show a stronger stroke than empty ones.
Invalid Implemented The invalid prop paints every cell in the danger stroke.
Disabled Implemented
Masked Implemented mask renders each cell as a password field.
Loading Not implemented

Usage

Do

  • Use onComplete to submit or verify once the final cell is filled, rather than polling onChange.
  • Keep mode="numeric" (the default) for digit-only codes so mobile keyboards show the number pad.
  • Provide a label (or aria-label) so the group has an accessible name.

Don't

  • Don't use InputOTP for general text entry — it's for short, fixed-length codes only.
  • Don't set a length that doesn't match the code you send; entry is capped at length.

Accessibility

Content

There’s no free text — content is the code itself. Keep the accompanying label short (Verification code) and put any “resend” affordance or error message outside the field.

Props API

React@ghds/react

PropTypeDefaultDescription
lengthnumber6Number of segments/cells.
valuestringControlled value (a left-filled prefix, never longer than length).
defaultValuestring''Initial value when uncontrolled.
onChange(value: string) => voidFires on every change with the full current value.
onComplete(value: string) => voidFires once the last cell is filled, with the complete code.
mode'numeric' | 'text''numeric'Accepted characters; 'numeric' restricts to digits.
maskbooleanfalseMasks the entered characters like a password.
disabledbooleanfalseDisables interaction.
invalidbooleanfalseMarks every cell invalid (danger stroke).
labelstringVisible label, associated with the group via Radix Label.
aria-labelstringAccessible name when no visible label is provided.

Web Components@ghds/web-components

PropTypeDefaultDescription
lengthnumber6Number of cells.
valuestring''Current value (a left-filled prefix).
mode'numeric' | 'text''numeric'Accepted characters.
maskbooleanfalseMasks the entered characters.
disabledbooleanfalseReflected attribute; disables entry.
invalidbooleanfalseReflected attribute; danger stroke on every cell.
labelstringOptional visible label, associated with the first cell.
eventsgh-change, gh-completeCustomEvents (detail: { value }) — gh-change on every edit, gh-complete once all cells are filled.

React Native@ghds/react-native

PropTypeDefaultDescription
lengthnumber6Number of cells.
valuestringControlled value.
defaultValuestring''Initial value when uncontrolled.
onChange(value: string) => voidFires on every change.
onComplete(value: string) => voidFires once the last cell is filled.
mode'numeric' | 'text''numeric'Accepted characters; drives the keyboardType.
maskbooleanfalsesecureTextEntry for the cells.
disabled / invalidbooleanfalseDisable, or mark every cell invalid.
label / accessibilityLabelstringVisible label, or an accessible name for the group.
testIDstringTest handle (applied to the cell group).

Code examples

// React — @ghds/react
import { InputOTP } from '@ghds/react/input-otp';

<InputOTP
  label="Verification code"
  value={code}
  onChange={setCode}
  onComplete={(code) => verify(code)}
/>;
// Invalid state:
<InputOTP label="Invalid code" defaultValue="123456" invalid />;
<!-- Web Components — @ghds/web-components -->
<script type="module">
  import '@ghds/web-components/input-otp';
</script>
<gh-input-otp id="otp" label="Verification code"></gh-input-otp>
<script>
  // Controlled-only; it dispatches gh-change on every edit and
  // gh-complete once every cell is filled (detail: { value }).
  document.getElementById('otp').addEventListener('gh-complete', (e) => {
    verify(e.detail.value);
  });
</script>
// React Native — @ghds/react-native
import { InputOTP } from '@ghds/react-native/input-otp';

<InputOTP label="Verification code" onComplete={(code) => verify(code)} />;

Live Demo

React and Web Components render live below, driven by the same tokens — toggle dark mode in the header to see both respond identically. React Native can’t run in a browser without additional react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.

React @ghds/react

Web Components @ghds/web-components