Migrating
Migrating from input-otp
Section titled “Migrating from input-otp”@rxova/react-otp-input shares input-otp’s single-input architecture, so the mental model carries over — you
gain spatial tap-to-edit, WebOTP, and a form-library-friendly onChange.
| input-otp | @rxova/react-otp-input | Notes |
|---|---|---|
maxLength | length | Number of slots |
value / onChange | value / onChange | Both emit a string |
onComplete | onComplete | Fires when the value fills |
render={({ slots }) => …} | render={({ slots }) => …} or the compound API | See below |
containerClassName | className | On the root |
pattern | mode or pattern | mode="numeric" | "alphanumeric" | "alpha", or a custom pattern |
pushPasswordManagerStrategy | (not needed) | No width hack; the field never shifts layout |
| — | slotInteraction | "spatial" (default) enables tap-to-edit |
| — | webOTP | Opt into WebOTP SMS retrieval |
| — | mask, placeholder, invalid, label | First-class |
Slots: from render prop to compound
Section titled “Slots: from render prop to compound”input-otp gives you a render prop and you hand-build the slot markup (the shadcn InputOTPSlot
pattern). That still works here via render. But the grouped 123–456 layout everyone copies is now
first-party:
// input-otp — render prop + your own slot component<OTP maxLength={6} render={({ slots }) => ( <> <div>{slots.slice(0, 3).map((s, i) => <Slot key={i} {...s} />)}</div> <Separator /> <div>{slots.slice(3).map((s, i) => <Slot key={i} {...s} />)}</div> </>)} />
// @rxova/react-otp-input — the compound API, shipped<OtpInput length={6} value={code} onChange={setCode} label="Code"> <OtpGroup><OtpSlot index={0} /><OtpSlot index={1} /><OtpSlot index={2} /></OtpGroup> <OtpSeparator>–</OtpSeparator> <OtpGroup><OtpSlot index={3} /><OtpSlot index={4} /><OtpSlot index={5} /></OtpGroup></OtpInput>What changes for the better
Section titled “What changes for the better”- Tap a middle slot to edit it — input-otp #32; it’s the default here.
- No password-manager width hack — no
pushPasswordManagerStrategy, no layout shift. translate="no"— no crash on Chrome auto-translate.- WebOTP —
webOTP/useWebOTP. - Forms —
<Controller>and nativenameboth work;onChangeis astring.
Styling
Section titled “Styling”input-otp’s data-active / data-char slot attributes map onto [data-active], [data-filled],
and [data-state]. There is no CSS to import — style with --otp-* tokens instead. See
Styling.
Migrating from react-otp-input
Section titled “Migrating from react-otp-input”react-otp-input renders N separate <input>s; @rxova/react-otp-input renders one. That single
change fixes a cluster of the N-input problems for free — but it also means the API shifts from
“configure N boxes” to “configure one field.”
| react-otp-input | @rxova/react-otp-input | Notes |
|---|---|---|
numInputs | length | Number of slots |
value / onChange | value / onChange | onChange emits a string (react-otp-input does too) |
renderInput | (not needed) | You don’t render N inputs; use the compound API or render |
renderSeparator | <OtpSeparator> | Part of the compound API |
inputType="number" | "tel" | "password" | mode + mask | mode="numeric"; mask for password style |
shouldAutoFocus | autoFocus | |
skipDefaultStyles | (always headless) | No styles to skip; use --otp-* tokens |
containerStyle / inputStyle | style / className + tokens | |
| — | onComplete, webOTP, slotInteraction, invalid, placeholder | New |
Before / after
Section titled “Before / after”// react-otp-input — N inputs, render each one<OtpInput value={code} onChange={setCode} numInputs={6} renderSeparator={<span>-</span>} renderInput={(props) => <input {...props} />}/>
// @rxova/react-otp-input — one field<OtpInput length={6} value={code} onChange={setCode} label="Code" />What the single input fixes
Section titled “What the single input fixes”- SMS autofill fills the whole code, not just the first box.
- No per-keystroke input-type churn — the mobile keyboard stays numeric (react-otp-input #327).
- Backspace, arrows, Home/End, paste, and selection are native — no hand-rolled focus juggling.
- One accessible field, one tab stop, one
label— instead of six unlabelled boxes. - Native form submission via
name; no hidden concat field.
Styling
Section titled “Styling”Replace inputStyle / focusStyle with --otp-* tokens and the [data-active] / [data-filled]
hooks. See Styling and Theming.