Using a Single Input for one-time-code
Tags: css, javascript, webdev, showdev
Published: 2023-06-12 08:05:01

The other night I had a bit of fun trying to create a single-input **one-time-code**:

![One-Time-Code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1rtk2r62uqs0m7txu71.png)

**one-time-code** is a valid `autocomplete`-value, and with a few lines of JS, it let's you fill out the field from a text-message (sms). 

More often, it's called **OTP**, though.

_[From Wikipedia:](https://en.wikipedia.org/wiki/One-time_password)_

> A one-time password (OTP), also known as a one-time PIN, one-time authorization code (OTAC) or dynamic password, is a password that is valid for only one login session or transaction, on a computer system or other digital device.

---

Back to my example. 

It's using *very* simple markup:

```html
<input
  type="text"
  autocomplete="one-time-code"
  inputmode="numeric"
  maxlength="6"
  pattern="\d{6}"
>
```

The CSS is a bit more complex:

```css
:where([autocomplete=one-time-code]) {
  --otp-digits: 6; /* length */
  --otc-ls: 2ch;
  --otc-gap: 1.25;
  /* private consts */
  --_otp-bgsz: calc(var(--otc-ls) + 1ch);

  all: unset;
  background: linear-gradient(90deg, 
    var(--otc-bg, #EEE) calc(var(--otc-gap) * var(--otc-ls)),
    transparent 0
  ) 0 0 / var(--_otp-bgsz) 100%;
  caret-color: var(--otc-cc, #333);
  clip-path: inset(0% calc(var(--otc-ls) / 2) 0% 0%);
  font-family: ui-monospace, monospace;
  font-size: var(--otc-fz, 2.5em);
  inline-size: calc(var(--otc-digits) * var(--_otp-bgsz));
  letter-spacing: var(--otc-ls);
  padding-block: var(--otc-pb, 1ch);
  padding-inline-start: calc(((var(--otc-ls) - 1ch) / 2) * var(--otc-gap));
}
```

It's a bunch of stuff simulating 6 fields (from the property `--otc-digits`), while — in reality — it's just a single `<input>`. The spacing between the "fields" is due to `letter-spacing`, and the gray "boxes" are from a `linear-gradient`.

It has to use a monospace-font, so the _magic value_ of `1ch` works — same applies to the `letter-spacing`. `1ch` equals the width of a zero.

---

## But why?

Have you ever created a OTP-component before? 

I'm writing "component", because it's typically a `<fieldset>` with six `<input>`s and a bunch of JavaScript to detect when you enter- or leave a field etc. 

When you fill out the field from the [Web OTP API](https://web.dev/sms-otp-form/), you need to split the value, and fill-in six fields instead of one.

With a single input it's **much** [simpler](https://web.dev/sms-otp-form/):

```js
navigator.credentials.get({
  otp: {transport:['sms']}
})
.then(otp => input.value = otp.code);
```
---

## Highlightning the current "field"

The single-input OTP is not perfect. When you move from "field" to "field", it would be a better user-experience, if the caret was a block: 

```css
.selector {
  caret-shape: block;
}
```

Unfortunately, no browsers support `caret-shape` yet.

---

Another way, would be to add *another* `background-gradient`, but without repeating the pattern:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z19wl0zh0d9ahhzz9ohw.png)

And position it by multiplying the size-property — `--_otp-bgsz` — with the digit-number, `--_otp-digit`, as a custom property:

```css
.selector {
  background-position: 
    calc(var(--_otp-digit, 0) * var(--_otp-bgsz)) 0;
}
```

This is not perfect, because we'd need to put the digit in a CSS Custom Property, and then update *that* with JavaScript:

```js
input.addEventListener('input', () => 
  input.style.setProperty('--_otp-digit', 
  input.selectionStart)
)
```

---

Could this be done in an even simpler way? Other sugestions? Please use the comments!

---

Here's a Codepen-demo:

{% codepen https://codepen.io/stoumann/pen/bGQdgpw %}