GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

OTP Input UI: 9 tình huống kèm validation

OTP input ui là phần form nhận sáu chữ số, mỗi số một ô, muốn mượt thì phải lo cả nhảy ô kế, dán nguyên mã lẫn xử lý lỗi, kèm cả validation.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục không xếp theo độ nổi tiếng mà theo đúng trình tự một form nhập mã OTP thật sự trải qua. Đầu tiên là dòng chảy bắt đầu nhập (01~04), kế đến xác nhận và bảo mật (05~06), rồi gửi lại và tự điền (07~08), cuối cùng là tiếp cận (09). Mỗi mục giữ đúng một vai trò trên màn hình và chỉ nhặt đúng thuộc tính, sự kiện cần cho khoảnh khắc đó.

01Tự động nhảy ô kế tiếp

Chuyển động thế nào

Gõ xong một số, con trỏ tự nhảy sang ô kế tiếp.

Tự chuyển sang ô kế tiếp
Mở trong tab mới

01 · Tự chuyển sang ô kế tiếp — Diện tích đo được 0.907% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

6칸자동 포커스숫자만

Khi nào dùng otp input field ui

Cho màn hình nhận sáu chữ số liền mạch như xác thực đăng ký hay đăng nhập hai bước.

boxes.forEach(function (b, i) {
  b.addEventListener('input', function () {
    b.value = b.value.replace(/[^0-9]/g, '').slice(-1);
    if (b.value && boxes[i + 1]) boxes[i + 1].focus({ preventScroll: true });
  });
});

02Backspace quay về ô trước

Bạn thấy gì

Nhấn Backspace ở ô trống, con trỏ quay về ô trước và xóa số ở đó.

Backspace về ô trước
Mở trong tab mới

02 · Backspace về ô trước — Diện tích đo được 0.888% · khung hình chuyển động 18/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

백스페이스이전 칸연쇄 삭제

Dùng ở đâu

Gõ nhầm giữa chừng thì không phải xóa cả chuỗi từ đầu.

boxes.forEach(function (b, i) {
  b.addEventListener('keydown', function (e) {
    if (e.key === 'Backspace' && !b.value && boxes[i - 1]) {
      boxes[i - 1].focus({ preventScroll: true });
    }
  });
});

03Dán cả mã, chia vào từng ô

Chuyện gì diễn ra trên màn hình

Dán mã sáu chữ số copy từ app mail hay tin nhắn là nó tự chia vào từng ô ngay một lần.

Dán cả mã chia vào từng ô
Mở trong tab mới

03 · Dán cả mã chia vào từng ô — Diện tích đo được 0.432% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

clipboard6칸 분배숫자 외 무시

Chỗ dùng

Ký tự không phải số bị lọc bỏ, ô thừa giữ nguyên.

pin.addEventListener('paste', function (e) {
  var t = (e.clipboardData || window.clipboardData)
    .getData('text').replace(/[^0-9]/g, '');
  if (!t) return;
  e.preventDefault();
  t.slice(0, 6).split('').forEach(function (ch, k) {
    if (boxes[k]) boxes[k].value = ch;
  });
});

04Ép bàn phím số trên mobile

Chuyển động thế nào

Trên mobile, bàn phím số bật thẳng thay cho bàn phím chữ.

Ép bàn phím số trên mobile
Mở trong tab mới

04 · Ép bàn phím số trên mobile — Diện tích đo được 0.8% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

inputmode numericpattern [0-9]*텐키

Dùng khi nào

Chỉ hai thuộc tính inputmode và pattern là đủ, otp input html css thuần không cần JS riêng.

<input
  type="text"
  inputmode="numeric"
  pattern="[0-9]*"
  maxlength="1"
  autocomplete="one-time-code"
  aria-label="Ô mã OTP thứ 1"
>

05Bật tắt chế độ che số

Bạn thấy gì

Bấm icon con mắt, các số đã nhập hiện rõ thay vì các dấu chấm.

Bật tắt chế độ che số
Mở trong tab mới

05 · Bật tắt chế độ che số — Diện tích đo được 0.702% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

마스킹토글점 대체

Dùng ở đâu

Hợp lúc bạn cần tự kiểm tra giá trị, như mật khẩu tài khoản trong app ngân hàng hay mã PIN thanh toán.

var masked = true;
toggle.addEventListener('click', function () {
  masked = !masked;
  boxes.forEach(function (b) {
    b.type = masked ? 'password' : 'text';
  });
  toggle.textContent = masked ? 'Xem số đang ẩn' : 'Ẩn lại';
});

06Lỗi thì rung và dọn sạch

Chuyện gì diễn ra trên màn hình

Nhập mã sai, cả hàng ô rung lên rồi tự động sạch trơn.

Rung và xoá khi sai
Mở trong tab mới

06 · Rung và xoá khi sai — Diện tích đo được 5.167% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

흔들림 300ms자동 비움빨간 테두리

Chỗ dùng

Trong lúc đó màn hình giữ một câu trạng thái để trình đọc màn hình cũng biết việc xác nhận đã thất bại.

confirmBtn.addEventListener('click', function () {
  var code = boxes.map(function (b) { return b.value; }).join('');
  if (code.length < 6 || code !== realCode) {
    pin.classList.add('is-error');
    boxes.forEach(function (b) { b.value = ''; });
  }
});

07Timer gửi lại

Chuyển động thế nào

Nút gửi lại chỉ bấm được sau khi đếm ngược hết 60 giây.

Đếm ngược gửi lại
Mở trong tab mới

07 · Đếm ngược gửi lại — Diện tích đo được 1.097% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

60초비활성카운트다운

Dùng khi nào

Cách này chặn việc yêu cầu mã SMS nhiều lần trong một khoảng thời gian ngắn.

var left = 60;
function tick() {
  resend.textContent = left > 0 ? left + ' giây nữa gửi lại' : 'Gửi lại';
  resend.disabled = left > 0;
  if (left > 0) { left--; setTimeout(tick, 1000); }
}
tick();

08Tự điền SMS OTP

Bạn thấy gì

Hệ điều hành nhận ra mã đến trong tin nhắn và tự điền vào các ô.

Tự điền SMS OTP
Mở trong tab mới

08 · Tự điền SMS OTP — Diện tích đo được 0.749% · khung hình chuyển động 9/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

autocompleteone-time-codeOS 감지

Dùng ở đâu

Chỉ một thuộc tính autocomplete="one-time-code" là chạy được cả trên iOS lẫn Android.

<input
  type="text"
  inputmode="numeric"
  autocomplete="one-time-code"
  maxlength="1"
>

09Focus tĩnh khi giảm chuyển động

Chuyện gì diễn ra trên màn hình

Với cài đặt giảm chuyển động, focus nhảy thẳng sang ô kế mà không có hiệu ứng.

Focus tĩnh khi giảm chuyển động
Mở trong tab mới

09 · Focus tĩnh khi giảm chuyển động — Diện tích đo được 1.001% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

prefers-reduced-motion즉시 이동정적 포커스

Chỗ dùng

Vừa giảm gánh nặng cho người nhạy tiền đình, vừa giữ nguyên tính năng tự nhảy ô.

@media (prefers-reduced-motion: reduce) {
  .pin__ghost, .pin__caret, .pin__signal {
    animation: none !important;
    transition: none !important;
  }
  .pin.is-demo .pin__ghost { opacity: 1; }
}

OTP input dễ hỏng ở đâu — cái bẫy

Lúc tự điền bỏ sót cả sáu ô

Gắn autocomplete="one-time-code" cho từng ô thì phần lớn trình duyệt mới tự điền mã SMS vào được, nhưng một vài Safari và trình duyệt Android cũ vẫn hay bỏ sót kiểu tự điền chia sáu ô. Mẫu form OTP của web.dev đề xuất phương án giữ sáu ô hiển thị nguyên vẹn, còn việc tự điền nhận qua một input ẩn ngoài màn hình rồi JS chia vào sáu ô khi giá trị tới. Mục 08 trong zip này chưa dùng phương án thỏa hiệp ấy, chỉ gắn thuộc tính từng ô, nên trước khi gắn vào dịch vụ thật bạn nên tự bấm thử trên vài máy để xác nhận tự điền thật sự chạy đúng. File nén mở bằng mật khẩu 39kzjrs9, chứa trọn vẹn cả chín mục đến tận 09. Sự kiện paste cũng có đúng cái bẫy này: gắn listener cho từng ô thì một lần dán có thể chạy chồng nhiều lần, phải gắn duy nhất vào container bọc ngoài như mục 03 thì mã mới chia đúng một lần.

Khả năng tiếp cận

Giảm chuyển động và trình đọc màn hình

Cả chín mục đều nghe theo tùy chọn giảm chuyển động của hệ điều hành. Ở 01·04·09, con trỏ và tín hiệu biến mất, các ô chỉ còn trạng thái đã điền; 05 đổi trạng thái che mà không chớp icon; 06 bỏ rung, chỉ giữ câu báo lỗi trên màn hình. Mỗi ô có aria-label đọc ra vị trí thứ mấy, còn câu lỗi của 06 dùng role="status" để trình đọc màn hình nhận thông báo riêng. Muốn nhấn mạnh xác thực bằng hiệu ứng thì chín hiệu ứng modal animation có sẵn phần rung lỗi và fade để tham khảo, còn hướng vận hành của trang nằm ở trang giới thiệu.

FAQ

Nhập mã PIN có bắt buộc chia thành sáu ô không?

Không. Một <input maxlength="6"> duy nhất cũng nhận được, nhưng chia ô giúp bạn thấy ngay còn thiếu mấy số và dễ đối xử khác nhau cho từng vị trí khi làm tự nhảy ô hay Backspace.

autocomplete="one-time-code" chạy trên trình duyệt nào?

Safari, Chrome, Edge bản mới đều nhận diện autocomplete="one-time-code" và tự điền mã đến qua SMS. Tổ hợp trình duyệt với hệ điều hành chính xác thì tra thêm trên caniuse.com, và nên thử kèm cách input ẩn đã nêu ở phần bẫy, nhất là khi bạn tự dựng otp input ui design cho dịch vụ thật.

Mã bốn chữ số thay vì sáu cũng dùng được chứ?

Được. React component trong zip chỉ cần đổi giá trị length là số ô chạy theo, nên mã PIN ngân hàng bốn số hay mã dự phòng tám số đều tái dùng đúng một cấu trúc.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.