Building a better SMS OTP form

SMS one-time passwords (OTPs) are a common way to verify identity during two-factor authentication, phone number verification, account recovery, or payment confirmation. The way you build the form that collects that code has a big impact on whether users can complete the flow quickly and without errors.

Start with the right input element

The single most important practice is to use a standard <input> element in a form. Every browser supports this, so even if more advanced features in this article fail, users can always enter and submit the OTP manually. Three attributes on that input make the biggest difference:

  • type="text"
  • inputmode="numeric"
  • autocomplete="one-time-code"

Why not type="number"?

Because OTPs are five or six digits, it is tempting to use type="number" to get a numeric keyboard on mobile. That is a mistake. Browsers treat a number input as a countable value, not a sequence of digits, which can lead to unexpected behavior. Spinner buttons appear next to the field, and incrementing or decrementing can strip leading zeros from the code.

Stick with type="text" and let inputmode="numeric" handle the keyboard. That attribute switches mobile devices to a numbers-only keypad on focus, which is exactly what a text input needs.

Some sites have used type="tel" as a hack to get the numeric keypad, since inputmode="numeric" was not widely supported for a time. Now that Firefox supports it, there is no reason to use a semantically incorrect type.

Enabling one-time-code autofill

Setting autocomplete="one-time-code" tells the browser what kind of data the field expects. When the user receives an SMS while the form is open, the operating system can parse the code and the keyboard will suggest it for entry. This works on Safari 12 and later on iOS, iPadOS, and macOS. It is not supported everywhere, but it is strongly recommended since it improves the experience on those platforms significantly.

Optional input attributes

A pattern attribute, using a regular expression, can constrain what the user submits. For example \d{6} limits the OTP to exactly six digits. The required attribute ensures the field is not left empty. Both are straightforward additions that catch user errors.

Format the SMS message correctly

The autofill attribute helps, but the real payoff comes from formatting the SMS message according to the origin-bound one-time codes specification. The rule is simple: end the message with the receiving domain preceded by @ and the OTP preceded by #.

That format makes extraction reliable and binds the code to your domain. If the user visits a different site, the OTP suggestion will not appear, which reduces the risk of phishing and account hijacking. Browsers no longer need to guess at the code with heuristics when the message follows the standard.

The precise rules are:

  • The message may begin with human-readable text, but the last line must contain the URL and the OTP.
  • The domain of the website that invoked the API is preceded by @.
  • A # precedes the OTP itself.
  • The domain and code together must be 140 characters or fewer.

Safari on iOS 14 or later reads this format when autocomplete="one-time-code" is present. Chrome, Opera, and Vivaldi on Android also follow the origin-bound format, but through the WebOTP API instead of the autocomplete attribute.

Using the WebOTP API

The WebOTP API gives JavaScript direct access to an OTP delivered by SMS. Call navigator.credentials.get() with an OTP credential type whose transport includes sms. The site then waits for a message that complies with the origin-bound format, and the user grants access to the code. Once the OTP reaches JavaScript, you can fill a form field or POST it straight to your server. Make sure your <form> has action and method attributes when using the copy-and-paste snippet approach.