Browse docs
Browse docs
A segmented input for one-time codes — verification codes, 2FA tokens, magic-link confirmations. Renders length cells side-by-side; sanitizes paste content per mode; AT-accessible via a single hidden <input> that absorbs keystrokes (so a screen reader announces "code field, 6 characters" not "6 separate inputs").
import { OTPField } from '@dashforge/tw';
<OTPField name="code" label="Verification code" length={6} />import { DashForm } from '@dashforge/forms';
import { OTPField, Button } from '@dashforge/tw';
<DashForm onSubmit={onSubmit}>
<OTPField
name="code"
label="6-digit code"
length={6}
mode="numeric"
required
onComplete={(code) => console.log('autofocus next step:', code)}
/>
<Button type="submit" color="primary">Verify</Button>
</DashForm>Standalone:
const [code, setCode] = useState('');
<OTPField
name="otp"
length={4}
value={code}
onChange={setCode}
/>Enter the 6-digit code we texted you.
import { OTPField } from '@dashforge/tw';
<OTPField
name="code"
label="Verification code"
length={6}
helperText="Enter the 6-digit code we texted you."
/>Letters and digits, case-insensitive.
import { OTPField } from '@dashforge/tw';
<OTPField
name="invite"
label="Invite code"
length={4}
mode="alphanumeric"
helperText="Letters and digits, case-insensitive."
/><OTPField name="otp" length={4} mode="numeric" /> {/* PIN */}
<OTPField name="otp" length={6} mode="numeric" /> {/* default 2FA */}
<OTPField name="otp" length={8} mode="alphanumeric" /> {/* longer codes */}mode="numeric" (default) restricts to digits 0-9. mode="alphanumeric" allows a-zA-Z0-9 (uppercase normalized).
Fires when all length slots are filled — perfect for auto-submitting verification flows:
<OTPField
name="code"
length={6}
onComplete={(code) => {
// Auto-submit when 6 digits entered
verifyCode(code).then(result => navigate(result.next));
}}
/>Pasting fills slots sequentially. If you paste "123456" and length={6}, all six cells fill in one operation. Excess characters truncate to length; characters that don't match mode are silently filtered.
<OTPField size="sm" length={4} />
<OTPField size="md" length={6} /> {/* default */}
<OTPField size="lg" length={6} /> {/* hero verification screen */}<OTPField
name="code"
length={6}
error
helperText="Code expired. Request a new one."
/><RadioGroup name="2faMethod" options={[{value:'sms'},{value:'app'}]} />
<OTPField
name="smsCode"
length={6}
visibleWhen={(engine) => engine.getNode('2faMethod')?.value === 'sms'}
onComplete={verifySms}
/>| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. |
length | number | 6 | Number of slots. |
mode | 'numeric' | 'alphanumeric' | 'numeric' | Allowed character set. |
label | ReactNode | — | Field label. |
required | boolean | false | Required marker + RHF rule. |
rules | RegisterOptions | — | RHF validation. |
size | 'sm' | 'md' | 'lg' | 'md' | Density. |
error | boolean | false | Force error state. |
disabled | boolean | false | Disable all slots. |
value / defaultValue | string | — | Controlled / uncontrolled value. |
onChange | (value: string) => void | — | Fires on every character change. |
onComplete | (value: string) => void | — | Fires when all length slots are filled. |
visibleWhen | (engine) => boolean | — | Reactive visibility. |
access | AccessRequirement | — | RBAC gating. |
slotProps | OTPFieldSlotProps | — | Per-slot overrides. |
sx | string | — | Utility-class override. |
root · label · requiredMark · slotsRow · slot · slotChar · hiddenInput · helperText · errorText
<input> of length length. Screen readers announce the field as one entity ("code, 6 characters") rather than 6 separate fields — the natural way users think about OTPs. The visible slots are presentation only; the hidden input owns focus and keyboard.mode are filtered silently. So pasting "AB-1234" into a length=6 mode=numeric field results in "1234" (4 slots filled, last 2 empty).onComplete vs onChange: onChange fires on every keystroke (use for live validation); onComplete fires once when the field is full (use for auto-submit). Both can coexist.inputmode="one-time-code". Our hidden input declares it — works on iOS Safari + recent Chrome Android.