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}
/>Configure <OTPField> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
OTPField: {
defaults: { size: 'md' },
},
},
});Configurable axes (OTPFieldVariantProps):
| Axis | Type | Notes |
|---|---|---|
size | 'sm' | 'md' | 'lg' | Slot cell size + font-size. |
Non-visual axes (name, rules, length, mode, label, helperText, error, disabled, access, visibleWhen, onChange, onComplete) are not theme-configurable — they carry per-instance semantics.
Precedence chain (lowest → highest):
defaultVariants from the internal otpFieldVariants recipe.theme.components.OTPField.defaults (application-wide).<OTPField size="lg" />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.OTPField.defaults autocompletes to size only; unrecognized fields are compile-time errors.
Reactivity: useComponentDefaults('OTPField') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Bridge field name (required when used inside DashFormProvider). |
access | AccessRequirement | — | RBAC access requirement (combines with explicit disabled). |
defaultValue | string | — | Default value (uncontrolled, standalone mode only). |
disabled | boolean | false | Disables all slots + hidden input. |
error | boolean | false | Explicit error semaphore. Overrides the bridge's auto-detected error. |
helperText | ReactNode | — | Helper line below the slot row. Auto-replaced by bridge error when invalid. |
label | ReactNode | — | Visible label above the slot row. |
length | number | — | Number of slots (default 6 — the SMS code convention). |
mode | OTPFieldMode | — | Character set allowed. Default 'numeric'. |
onChange | (value: string) => void | — | Fires every time the joined value changes (sanitised, ≤ length). |
onComplete | (value: string) => void | — | Fires when the user has filled all slots. |
required | boolean | false | Renders the required * marker + sets the native required attribute. |
rules | unknown | — | RHF validation rules — opaque, forwarded to the bridge. |
size | 'md' | 'sm' | 'lg' | 'md' | Density tier — drives slot cell size + typed-character font. |
slotProps | OTPFieldSlotProps | — | Per-slot className overrides. |
sx | string | — | Root className shortcut. |
value | string | — | Controlled value (form mode reads from the bridge if omitted). |
visibleWhen | (engine: Engine) => boolean | — | Engine predicate — field not rendered when it returns false. |
<OTPField> is a compound component with 9 named slots. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer flex wrapper (label + slotsRow + helper/error). |
label | The <label> element. |
requiredMark | The red * next to the label when required. |
slotsRow | The container holding the visible slot cells + the hidden input. |
slot | Each individual slot cell (length of them). |
slotChar | The <span> inside a slot displaying the typed character. |
hiddenInput | The invisible <input> that captures keystrokes for a11y + SMS autofill. |
helperText | Helper text line, when not in error state. |
errorText | Helper text line, when in error state. |
Use sx for a root-level class override, slotProps for a specific inner element (e.g. slot border color, slotChar font-weight).
<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.