sdxc

Type to search, or start from one of these:

OtpField

A one-time-code field.

We sent a six-digit code to sergio@example.com. Resend.

As separate slots

Typing moves to the next slot, Backspace returns to the last one, and pasting the whole code fills every slot at once.

The single field submits as one value; the slot group reports its own through the mixin's completion event.

View code
let code: string | null = null;

<form method="post" action="/login/verify" mix={[vstack({ gap: 5, align: "stretch" })]}>
	<div mix={[vstack({ gap: 2, align: "stretch" })]}>
		<Label htmlFor="otp">One-time code</Label>
		<OtpField id="otp" name="code" aria-describedby="otp-hint" />
		<Description id="otp-hint">
			We sent a six-digit code to sergio@example.com. <Link href="/login/resend">Resend</Link>.
		</Description>
	</div>

	<div mix={[vstack({ gap: 2, align: "stretch" })]}>
		<Header mix={[m(0)]}>As separate slots</Header>
		{/* Each slot takes focus on its own and rings itself, so the row carries the group's
		    name and its layout and leaves the focus indicator to whichever slot holds it. */}
		<div
			role="group"
			aria-label="One-time code"
			mix={[
				hstack({ gap: 2, align: "center" }),
				otpSlots(),
				on<HTMLDivElement, "ui:otp-complete">("ui:otp-complete", (event) => {
					code = event.code;
					void handle.update();
				}),
			]}
		>
			{SLOTS.map((position) => (
				<OtpField
					key={position}
					length={1}
					data-otp-slot
					aria-label={"Digit " + position}
					autoComplete={position === 1 ? "one-time-code" : "off"}
					mix={[is("2.75rem")]}
				/>
			))}
		</div>
		<Description>
			Typing moves to the next slot, Backspace returns to the last one, and pasting the whole
			code fills every slot at once.
		</Description>
	</div>

	<Button type="submit" disabled={code === null}>
		{code === null ? "Enter the code" : "Verify " + code}
	</Button>

	<p mix={[m(0), text("xs")]}>
		The single field submits as one value; the slot group reports its own through the
		mixin's completion event.
	</p>
</form>

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { OtpField } from "@sdxc/ui";

Renders a single native <input> for entering a one-time verification code, built on Input, sized as one full-width control since a single <input> is what carries autoComplete="one-time-code" reliably.

Examples

<OtpField
	id="otp"
	name="code"
	aria-describedby="otp-error"
	aria-invalid="true"
/>
<FieldError id="otp-error">{t("auth.otp.invalid")}</FieldError>
<OtpField
	id="invite-code"
	name="inviteCode"
	length={8}
	inputMode="text"
	autoComplete="off"
	pattern="[A-Za-z0-9]*"
/>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
length?numberThe one-time code's character count, applied as the control's native maxLength. Defaults to DEFAULT_LENGTH.
inputMode?Input.Props["inputMode"]Virtual-keyboard hint passed straight through to the native inputMode attribute. Defaults to DEFAULT_INPUT_MODE, requesting the numeric keypad; set it to "text" for a code that mixes in letters.
autoComplete?Input.Props["autoComplete"]Platform autofill hint passed straight through to the native autoComplete attribute. Defaults to DEFAULT_AUTO_COMPLETE, which a browser or OS keyboard looks for to offer an SMS or email code inline.

Also accepts everything in Omit< Input.Props, "type" | "list" | "role" | "inputMode" | "autoComplete" | "maxLength" >.