Summary
Add a RubyUI port of shadcn/ui's InputOTP (demo source) — a one-time-code / PIN input.
shadcn's version wraps input-otp, which is React-only, so this is a from-scratch Stimulus reimplementation of the same behavior, not a wrapped JS package.
API (compound, mirrors shadcn 1:1)
InputOtp(length: 6, name: "otp", value: nil, pattern: nil, disabled: false) do
InputOtpGroup do
InputOtpSlot(index: 0)
InputOtpSlot(index: 1)
InputOtpSlot(index: 2)
end
InputOtpSeparator()
InputOtpGroup do
InputOtpSlot(index: 3)
InputOtpSlot(index: 4)
InputOtpSlot(index: 5)
end
end
InputOtp (not InputOTP — no acronym casing, matches existing NativeSelect/DataTable precedent).
Architecture
Single real <input> (not N per-slot inputs), visually transparent and absolutely positioned over the slot row, but kept in the accessibility tree (not display:none) — same core technique as the upstream input-otp lib. Visible slots are decorative aria-hidden <div>s painted by the Stimulus controller from the real input's value + selection state.
Required behavior (must actually work, not just be theoretically supported)
- Typing a digit auto-advances to the next slot
- Backspace deletes current slot and moves back
- ArrowLeft/ArrowRight move the active slot
- ArrowUp/ArrowDown also move the active slot (non-native on a single-line input — needs explicit
keydown handling)
- Paste distributes characters across slots, filtered by
pattern
- Dispatches a custom
ruby-ui--input-otp:complete event (bubbles, detail: { value }) when filled, so Hotwire/Stimulus consumers can hook in (e.g. auto-submit a form)
Accessibility
- The real input is the sole control with a value in the accessibility tree — normal
aria-label/<label> semantics
- Slots are
aria-hidden="true" (decorative duplicates, avoid double-announcing)
autocomplete="one-time-code" for SMS autofill
inputmode="numeric" by default
Out of scope (v1)
Password-manager badge width-push hack, <noscript> CSS fallback, iOS letter-spacing hacks from the upstream React lib — edge-case polish, not core OTP behavior. Can be follow-up issues if needed.
Files
gem/lib/ruby_ui/input_otp/{input_otp,input_otp_group,input_otp_slot,input_otp_separator}.rb
gem/lib/ruby_ui/input_otp/input_otp_controller.js
gem/lib/ruby_ui/input_otp/input_otp_docs.rb
gem/test/ruby_ui/input_otp_test.rb
docs/app/views/docs/input_otp.rb + routes/menu/site_files/controllers-index.js wiring
Full design: see design/2026-06-30-input-otp-design.md on the implementation branch.
Summary
Add a RubyUI port of shadcn/ui's InputOTP (demo source) — a one-time-code / PIN input.
shadcn's version wraps
input-otp, which is React-only, so this is a from-scratch Stimulus reimplementation of the same behavior, not a wrapped JS package.API (compound, mirrors shadcn 1:1)
InputOtp(notInputOTP— no acronym casing, matches existingNativeSelect/DataTableprecedent).Architecture
Single real
<input>(not N per-slot inputs), visually transparent and absolutely positioned over the slot row, but kept in the accessibility tree (notdisplay:none) — same core technique as the upstreaminput-otplib. Visible slots are decorativearia-hidden<div>s painted by the Stimulus controller from the real input's value + selection state.Required behavior (must actually work, not just be theoretically supported)
keydownhandling)patternruby-ui--input-otp:completeevent (bubbles,detail: { value }) when filled, so Hotwire/Stimulus consumers can hook in (e.g. auto-submit a form)Accessibility
aria-label/<label>semanticsaria-hidden="true"(decorative duplicates, avoid double-announcing)autocomplete="one-time-code"for SMS autofillinputmode="numeric"by defaultOut of scope (v1)
Password-manager badge width-push hack,
<noscript>CSS fallback, iOS letter-spacing hacks from the upstream React lib — edge-case polish, not core OTP behavior. Can be follow-up issues if needed.Files
gem/lib/ruby_ui/input_otp/{input_otp,input_otp_group,input_otp_slot,input_otp_separator}.rbgem/lib/ruby_ui/input_otp/input_otp_controller.jsgem/lib/ruby_ui/input_otp/input_otp_docs.rbgem/test/ruby_ui/input_otp_test.rbdocs/app/views/docs/input_otp.rb+ routes/menu/site_files/controllers-index.js wiringFull design: see
design/2026-06-30-input-otp-design.mdon the implementation branch.