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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions