OTP input — Migrating
Migrating to 1.0
Section titled “Migrating to 1.0”1.0 renames every styling hook. --otp-* becomes --rx-otp-* and [data-otp-*] becomes
[data-rx-otp-*]; nothing else about the API changed, and no behaviour changed with it.
| Before | After |
|---|---|
--otp-slot-size | --rx-otp-slot-size |
--otp-gap | --rx-otp-gap |
--otp-active-ring | --rx-otp-active-ring |
--otp-caret-color | --rx-otp-caret-color |
| …and the other ten | same --rx-otp- prefix |
[data-otp-root] | [data-rx-otp-root] |
[data-otp-slot] | [data-rx-otp-slot] |
[data-otp-input] | [data-rx-otp-input] |
[data-otp-group] | [data-rx-otp-group] |
[data-otp-separator] | [data-rx-otp-separator] |
[data-otp-caret] | [data-rx-otp-caret] |
The state hooks are unchanged: data-state, data-filled, data-active, data-disabled,
data-readonly, data-invalid. They mean the same thing on every input in the suite, so one
selector should keep reaching all of them.
One command, stylesheets included:
npx @rxova/codemod rx-token-prefixes --extensions css,scss,tsx,ts,jsx,js ./srcPass --dry first to review. Running it twice is a no-op, and quote style and formatting survive
because nothing is reparsed.
Why: the old prefixes were each package’s initials, which stops working at nine components —
password and phone both reduce to rpi. Custom properties inherit, so setting the wrong one is
silently inert rather than an error. --rx-<component>-* is collision-free by construction and is
now enforced in CI.
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 --rx-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 --rx-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 --rx-otp-* tokens and the [data-active] / [data-filled]
hooks. See Styling and Theming.