Back to News & Insights
Web Development October 4, 2026 ยท 8 min read

๐Ÿ” OTP RS: RFC-Compliant OTP Input Components for WASM Frameworks

Nว hวŽo ๐Ÿ‘‹! You've seen the textbox. You've typed the number. You've waited three seconds for the...

๐Ÿ” OTP RS: RFC-Compliant OTP Input Components for WASM Frameworks

You've seen the textbox. You've typed the number. You've waited three seconds for the SMS that never came, checked your spam folder, clicked "Resend", typed the number again, and finally admitted to yourself that 2FA is simultaneously the most important and most annoying invention of the modern web.

Today, we make it marginally less annoying, at least on the Rust side. We're shipping OTP RS.

OTP RS is a composable, accessible, animated One-Time Password input component for Yew, Dioxus, and Leptos. It implements the full verifier flow: slot-by-slot input, keyboard navigation, active/filled/invalid/disabled states, animated caret blink, digit-entry pop animation, and RFC 4226 (HOTP) + RFC 6238 (TOTP) validation, all in Rust, all in WASM, all without touching a single line of JavaScript.

If you've ever tried to build a 6-box OTP input from scratch, you know the drill. It starts with "just six inputs". Two hours later you're debugging focus management, backspace propagation, keyboard event edge cases, browser autofill hijacking your state, and the invisible iOS number pad refusing to appear. That's the origin story of OTP RS.

OTP RS follows the same composable anatomy as every other component in the Open SASS Kit. You compose what you need: Otp: Root container. Owns the internal value state, focus coordination, and the RFC validation callbacks. Group: A visual cluster of slots (e.g., three digits, then a separator, then three more). Slot: Individual slot. Renders a visible character display on top of an invisible overlay . Separator: The little dash in between groups.

That's it. Focus management, digit propagation, state aggregation, and ARIA attributes are all handled. You just compose slots.

Here's the most important design decision in all of OTP RS: the element that receives keyboard events is completely invisible.

Each Slot renders two layers: An invisible (via input-rs with otpmode=true) that is position: absolute; inset: 0; opacity: 0. It captures all keyboard focus and input events. A visible that displays the digit character and the blinking caret.

This separates the input mechanics from the visual presentation, giving us full control over the slot's look without fighting browser defaults for styling.

The slot design described above only works if the CSS for the invisible input is applied as an inline style attribute, not as a CSS class attribute. The component originally passed the overlay CSS string to inputclass, a prop that sets the HTML class="" attribute.

Browsers don't style elements based on CSS property strings stuck in the class attribute. They look up class names in stylesheets. So what we got instead of an invisible overlay was a white rectangle aggressively covering the entire OTP component, like a censored emoji on live TV.

The fix was to add an inputstyle: &'static str prop to input-rs in all three framework implementations, and apply it as style="{props.inputstyle}" on the bare otpmode . One line per framework. Six characters (style vs class). Three frameworks. Three PRs worth of suffering.

We tried calling usestate(...) inside an html! macro attribute. Yew forbids calling hooks anywhere except directly inside function component bodies. The macro expands to a closure, and hook calls need to be at the top level. Extracting usestate calls to let bindings before html! fixed it.

Dioxus and Leptos's input-rs props require &'static str. But our slot-specific strings like "Digit 3" are computed at runtime. The only way to produce a &'static str from a runtime String without pulling in a string interner is to Box::leak:

Yes, this leaks memory on every slot render. Each Slot render leaks ~15 bytes. Is this ideal? No. Does it compile? Yes. Is it the correct long-term solution? The correct long-term solution is a Signal prop type in input-rs, which is a future PR. For now: Box::leak and move on.

The macro sees the as keyword and gets confused about what's a prop name vs. a type expression. The fix: extract to a let binding before the view! call:

Three lines in, five compilation cycles later. We hope this saves you ten minutes.

Dioxus's rsx! macro is strict about attribute types. When slotclass was a String, the macro would refuse:

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation