30 CSS Login Forms25 / 30

CSS + JSMIT licensed

OTP Verify

The multi-factor confirmation screen: six evenly-sized digit boxes in a grid that auto-advance as you type, accept a full paste, and backspace back — the standard email / SMS code entry, done accessibly.

Published Updated

Live Demo
Try it

The code

<section class="lf-25">
  <form class="lf-25__card" novalidate>
    <h1 class="lf-25__h">Verify your identity</h1>
    <p class="lf-25__sub">Enter the 6-digit code we sent to your email.</p>
    <div class="lf-25__otp" role="group" aria-label="6-digit verification code">
      <input class="lf-25__box" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
    </div>
    <button class="lf-25__submit" type="submit">Verify</button>
    <p class="lf-25__foot">Didn't get it? <a href="#">Resend code</a></p>
  </form>
</section>
.lf-25,
.lf-25 *,
.lf-25 *::before,
.lf-25 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.lf-25 {
  --accent: oklch(0.56 0.18 265);
  --box: 52px;
  --radius: 11px;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  background: #eef1f7;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.lf-25__card {
  width: min(400px,100%);
  background: #fff;
  border: 1px solid #e5e9f2;
  border-radius: 18px;
  padding: 34px 30px;
  text-align: center;
  box-shadow: 0 22px 50px -38px rgba(16,24,60,.5);
}

.lf-25__h {
  font-size: 22px;
  font-weight: 700;
  color: #1a2237;
}

.lf-25__sub {
  font-size: 13.5px;
  color: #767d97;
  margin: 6px 0 22px;
  line-height: 1.5;
}

.lf-25__otp {
  display: grid;
  grid-template-columns: repeat(6,1fr);
  gap: 9px;
  margin-bottom: 22px;
}

.lf-25__box {
  height: var(--box);
  min-width: 0;
  text-align: center;
  font-size: 22px;
  font-weight: 700;
  color: #1a2237;
  border: 1.5px solid #d5dbe8;
  border-radius: var(--radius);
  background: #fafbfe;
  transition: border-color .16s,box-shadow .16s,transform .12s;
}

.lf-25__box::placeholder {
  color: #c3c9d8;
}

.lf-25__box:not(:placeholder-shown) {
  border-color: var(--accent);
  background: oklch(0.56 0.18 265/.05);
}

.lf-25__box:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px oklch(0.56 0.18 265/.2);
  transform: translateY(-2px);
}

.lf-25__submit {
  width: 100%;
  height: 50px;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  background: var(--accent);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  transition: filter .18s,transform .12s;
}

.lf-25__submit:hover {
  filter: brightness(1.07);
}

.lf-25__submit:active {
  transform: scale(.99);
}

.lf-25__submit:focus-visible {
  outline: 3px solid oklch(0.56 0.18 265/.5);
  outline-offset: 2px;
}

.lf-25__foot {
  font-size: 13px;
  color: #767d97;
  margin-top: 16px;
}

.lf-25__foot a {
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
}

.lf-25__foot a:hover {
  text-decoration: underline;
}

@media (max-width: 420px) {
  .lf-25 {
    --box: 46px;
  }

  .lf-25__box {
    font-size: 19px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lf-25 * {
    transition: none!important;
  }
}
const boxes = [...document.querySelectorAll('.lf-25__box')];
boxes.forEach((box, i) => {
  box.addEventListener('input', () => {
    box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
    if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
  });
  box.addEventListener('keydown', (e) => {
    if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
  });
  box.addEventListener('paste', (e) => {
    e.preventDefault();
    const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
    boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
    boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
  });
});
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Login Form from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: OTP Verify
Source: https://codefronts.com/components/css-login-forms/otp-verify/

The multi-factor confirmation screen: six evenly-sized digit boxes in a grid that auto-advance as you type, accept a full paste, and backspace back — the standard email / SMS code entry, done accessibly.
## HTML
```html
<section class="lf-25">
  <form class="lf-25__card" novalidate>
    <h1 class="lf-25__h">Verify your identity</h1>
    <p class="lf-25__sub">Enter the 6-digit code we sent to your email.</p>
    <div class="lf-25__otp" role="group" aria-label="6-digit verification code">
      <input class="lf-25__box" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 1">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 2">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 3">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 4">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 5">
      <input class="lf-25__box" type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" placeholder="•" aria-label="Digit 6">
    </div>
    <button class="lf-25__submit" type="submit">Verify</button>
    <p class="lf-25__foot">Didn't get it? <a href="#">Resend code</a></p>
  </form>
</section>
```
## CSS
```css
.lf-25,
.lf-25 *,
.lf-25 *::before,
.lf-25 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.lf-25 {
  --accent: oklch(0.56 0.18 265);
  --box: 52px;
  --radius: 11px;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  background: #eef1f7;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.lf-25__card {
  width: min(400px,100%);
  background: #fff;
  border: 1px solid #e5e9f2;
  border-radius: 18px;
  padding: 34px 30px;
  text-align: center;
  box-shadow: 0 22px 50px -38px rgba(16,24,60,.5);
}

.lf-25__h {
  font-size: 22px;
  font-weight: 700;
  color: #1a2237;
}

.lf-25__sub {
  font-size: 13.5px;
  color: #767d97;
  margin: 6px 0 22px;
  line-height: 1.5;
}

.lf-25__otp {
  display: grid;
  grid-template-columns: repeat(6,1fr);
  gap: 9px;
  margin-bottom: 22px;
}

.lf-25__box {
  height: var(--box);
  min-width: 0;
  text-align: center;
  font-size: 22px;
  font-weight: 700;
  color: #1a2237;
  border: 1.5px solid #d5dbe8;
  border-radius: var(--radius);
  background: #fafbfe;
  transition: border-color .16s,box-shadow .16s,transform .12s;
}

.lf-25__box::placeholder {
  color: #c3c9d8;
}

.lf-25__box:not(:placeholder-shown) {
  border-color: var(--accent);
  background: oklch(0.56 0.18 265/.05);
}

.lf-25__box:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px oklch(0.56 0.18 265/.2);
  transform: translateY(-2px);
}

.lf-25__submit {
  width: 100%;
  height: 50px;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  background: var(--accent);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  transition: filter .18s,transform .12s;
}

.lf-25__submit:hover {
  filter: brightness(1.07);
}

.lf-25__submit:active {
  transform: scale(.99);
}

.lf-25__submit:focus-visible {
  outline: 3px solid oklch(0.56 0.18 265/.5);
  outline-offset: 2px;
}

.lf-25__foot {
  font-size: 13px;
  color: #767d97;
  margin-top: 16px;
}

.lf-25__foot a {
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
}

.lf-25__foot a:hover {
  text-decoration: underline;
}

@media (max-width: 420px) {
  .lf-25 {
    --box: 46px;
  }

  .lf-25__box {
    font-size: 19px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .lf-25 * {
    transition: none!important;
  }
}
```

## JavaScript
```js
const boxes = [...document.querySelectorAll('.lf-25__box')];
boxes.forEach((box, i) => {
  box.addEventListener('input', () => {
    box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
    if (box.value && i < boxes.length - 1) boxes[i + 1].focus();
  });
  box.addEventListener('keydown', (e) => {
    if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
  });
  box.addEventListener('paste', (e) => {
    e.preventDefault();
    const digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
    boxes.forEach((b, j) => { if (digits[j] != null) b.value = digits[j]; });
    boxes[Math.min(digits.length, boxes.length) - 1]?.focus();
  });
});
```

How this works

The boxes are a CSS Grid (grid-template-columns: repeat(6,1fr)) of identically-sized single-character inputs with centred text, so they line up perfectly at any width. Each is a real <input inputmode="numeric"> so mobile shows the number pad.

A small script handles what CSS can't: typing a digit focuses the next box, Backspace on an empty box steps back, and pasting a 6-digit code fills every box at once. The active box gets a clear focus ring; a filled box gets an accent border via :not(:placeholder-shown).

The group is wrapped with an accessible label so screen readers announce it as one code field.

Make it yours

  • Change code length by adding boxes and updating the grid + script length.
  • Recolour focus / filled states from --accent.
  • Resize boxes via --box.
  • Add an auto-submit when the last box fills.

Gotchas — read before shipping

  • Support paste — users paste codes from email; splitting the pasted value across boxes is expected.
  • Use inputmode="numeric" + autocomplete="one-time-code" so mobile OS can auto-fill the SMS code.
  • Keep each box a real focusable input so Tab and screen readers work.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 60+.

Auto-advance and paste need JS; the grid, centring and filled/focus states are CSS.

Techniques used in this demo

Search CodeFronts

Loading…