Skip to content

[Feature] Add InputOtp component #449

Description

@djalmaaraujo

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions