GODRICH

9 ID Verification Screen UI Patterns, Paste-Ready

An id verification screen ui is a screen — a permission prompt, an OTP grid, a security question card — that proves a person is who they say they are before an

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below follows the path a real account actually takes, not a popularity list. It opens with the permission prompt an app shows before it can even check anything, moves through the checks that prove a human is present (captcha, OTP, a security question), then covers the two ways a person gets back into a locked account (recovery picker, onboarding after verification), and closes with the screens that manage an already-verified session (linked accounts, a timeout warning, and signing out). Each demo keeps a single screen centered on its own page so you can lift out one piece at a time.

01Permission request modal

When the app asks for camera or location access, the icon bounces gently in place, and the allow button carries a solid fill next to deny's plain outline so the safer default stands out. It's the screen right before an identity check itself, used for camera or mic prompts, or a first-launch notification permission screen.

@keyframes 아이콘 바운스포커스 트랩aria-modal
(function(){
  var scene = document.querySelector('.perm-scene');
  var modal = scene.querySelector('.perm-scene__modal');
  var deny = scene.querySelector('.perm-scene__btn--ghost');
  var allow = scene.querySelector('.perm-scene__btn--primary');
  function close(){ scene.classList.remove('is-demo'); scene.classList.add('is-closed'); }
  deny.addEventListener('click', close);
  allow.addEventListener('click', close);
  modal.addEventListener('keydown', function(e){
    if (e.key === 'Escape') close();
  });
}());

02Captcha verify checkbox

Clicking the box spins a brief loader, then swaps to a green check while a verified badge pops in with a springy overshoot. It's the human-check step right before an OTP goes out, and it doubles as spam prevention on a free-trial signup form.

@keyframes spin → scalerotate(560deg) 스피너:checked 상태
.captcha__input:checked ~ .captcha__box .captcha__check {
  opacity: 1;
  transform: scale(1);
  transition: opacity $dur-quick $ease-out, transform $dur-quick $ease-pop;
}
.captcha__input:checked ~ .captcha__box { border-color: $color; }
.captcha__input:focus-visible ~ .captcha__box { outline: 2px solid $color; outline-offset: 2px; }

@keyframes capt-check {
  0%, 28%   { opacity: 0; transform: scale(.4); }
  40%       { opacity: 1; transform: scale(1.15); }
  46%, 84%  { opacity: 1; transform: scale(1); }
  92%, 100% { opacity: 0; transform: scale(.4); }
}

03OTP code input grid

Six digit boxes sit in a row; typing in one auto-advances to the next while its border flashes blue for a beat. It's built for a 6-digit SMS code, the same layout that shows up on almost every two-factor authentication screen.

input 이벤트 자동 이동paste 이벤트 6자리 분배inputmode=numeric
boxes.forEach(function(box, i){
  box.addEventListener('input', function(){
    box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
    if (box.value && boxes[i + 1]) boxes[i + 1].focus({ preventScroll: true });
  });
  box.addEventListener('keydown', function(e){
    if (e.key === 'Backspace' && !box.value && boxes[i - 1]) boxes[i - 1].focus({ preventScroll: true });
  });
  box.addEventListener('paste', function(e){
    e.preventDefault();
    var digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
    boxes.forEach(function(b, j){ b.value = digits[j] || ''; });
    (boxes[Math.min(digits.length, boxes.length) - 1] || boxes[0]).focus({ preventScroll: true });
  });
});

04Security question card

A pre-registered question sits on the card while a red warning glow pulses around it and the answer field, cooling to plain white and a muted gray between pulses. It's the fallback screen for when an OTP can't be delivered, and it also covers a secondary check on an admin account.

@keyframes sq-panelbox-shadow 글로우aria-label
.sq-card__input:focus-visible { border-color: $subject-blue; box-shadow: 0 0 0 3px rgba($subject-blue, .25); }

@keyframes sq-panel {
  0%, 8%    { background: mix($color, white, 24%); box-shadow: 0 0 0 3px rgba($color, .3), 0 0 34px 18px rgba($color, .3), $shadow-raised; }
  23%, 77%  { background: $stage-paper; box-shadow: $shadow-raised; }
  92%, 100% { background: mix($color, white, 24%); box-shadow: 0 0 0 3px rgba($color, .3), 0 0 34px 18px rgba($color, .3), $shadow-raised; }
}
@keyframes sq-input {
  0%, 8%    { border-color: $color; box-shadow: 0 0 0 4px rgba($color, .22), 0 0 20px 10px rgba($color, .25); }
  23%, 77%  { border-color: #d8d2c4; box-shadow: none; }
  92%, 100% { border-color: $color; box-shadow: 0 0 0 4px rgba($color, .22), 0 0 20px 10px rgba($color, .25); }
}

05Recovery method picker

Email, text, and security-question options sit as radio cards, and picking one lifts just that card into a bright highlight on the dark backdrop. It's the first screen a person sees when choosing a recovery channel after forgetting a password, and it's also a check before unlocking a locked account.

role=radiogroup:has(:checked)@keyframes picker-cycle
.recovery__card:has(.recovery__input:checked) {
  background: mix($color, $stage-ink, 26%);
  border-color: $color;
  box-shadow: 0 0 0 3px rgba($color, .3), 0 0 24px 12px rgba($color, .28);
  transform: scale(1.04);
}
.recovery__card:has(.recovery__input:focus-visible) { outline: 2px solid $color; outline-offset: 2px; }

.recovery.is-demo .recovery__card { animation: picker-cycle $duration $easing infinite; }
.recovery.is-demo .recovery__card:nth-child(1) { animation-delay: 0ms; }
.recovery.is-demo .recovery__card:nth-child(2) { animation-delay: 660ms; }
.recovery.is-demo .recovery__card:nth-child(3) { animation-delay: 1320ms; }

06Onboarding wizard step card

Right after verification, a profile-setup card slides its content to the left when you tap next, and the following pastel-toned card slides in to replace it. It's the profile-setup step that follows signup verification, and the same pattern collects information across a first-run onboarding flow.

transform: translateX 슬라이드커스텀 프로퍼티 --step@keyframes wiz-track
(function(){
  var track = document.querySelector('.wizard__track');
  var dots = Array.prototype.slice.call(document.querySelectorAll('.wizard__dot'));
  var btn = document.querySelector('.wizard__next');
  var step = 0;
  btn.addEventListener('click', function(){
    step = step === 0 ? 1 : 0;
    track.style.transform = 'translateX(' + (step === 0 ? '0%' : '-50%') + ')';
    dots.forEach(function(d, i){ d.classList.toggle('is-active', i === step); });
    btn.textContent = step === 0 ? 'Next' : 'Back';
  });
}());

07Linked accounts list

Login providers like Google or GitHub sit in rows, and flipping the connect switch slides the toggle over while the row tints with a soft background color. It's the social-login management screen in account settings, the one dashboard that unifies every sign-in method a person has linked.

checkbox 기반 토글transform: translateXfocus-visible 링
.accounts__input:checked ~ .accounts__track { background: $color; }
.accounts__input:checked ~ .accounts__track .accounts__knob { transform: translateX(14px); }
.accounts__input:focus-visible ~ .accounts__track { outline: 2px solid $color; outline-offset: 2px; }
.accounts__row:has(.accounts__input:checked) { background: rgba($subject-mint, .18); }

@keyframes acct-knob-cycle {
  0%, 8%    { transform: translateX(0); }
  14%, 36%  { transform: translateX(14px); }
  42%, 100% { transform: translateX(0); }
}

08Session timeout warning banner

A banner slides down from the top and counts down the remaining seconds, and tapping extend resets the timer back to 30 without dismissing the banner. It's the auto-logout warning on a banking or payment app, and the same countdown covers an idle-session alert in an admin console.

transform: translateY 슬라이드setInterval 카운트다운role=alert
(function(){
  var cnt = document.querySelector('.timeout-banner__count'),
      btn = document.querySelector('.timeout-banner__extend'),
      n = 30,
      timer = setInterval(function(){
        n--; if (n < 0) n = 0; cnt.textContent = n;
      }, 1000);
  btn.addEventListener('click', function(){
    n = 30; cnt.textContent = n;
  });
}());

09Sign-out confirmation card

The card shakes and washes red in a recurring warning loop, and hovering the final leave button deepens its background to a darker red. It's a double-check against an accidental sign-out tap, and it's also the screen that warns before signing out every device at once.

@keyframes shake커스텀 프로퍼티 --dangerfocus-visible 링
(function(){
  var card = document.querySelector('.signout-card'),
      cancel = card.querySelector('.signout-card__btn--cancel'),
      danger = card.querySelector('.signout-card__btn--danger');
  cancel.addEventListener('click', function(){ card.classList.remove('is-confirmed'); });
  danger.addEventListener('click', function(){ card.classList.add('is-confirmed'); });
}());

Where it breaks — the trap

The real trap while building these nine showed up in item 03, the OTP code grid. Calling a plain .focus() on the auto-advance step makes the browser scroll the newly focused element into view by default, and at a 320px mobile width, where the input sits inside a scroll container, every auto-advance caused a tiny scroll jump even though the next box was already fully visible. The fix was adding { preventScroll: true } to every .focus() call in the grid, as shown in the code above, and MDN's focus() preventScroll documentation confirms this is exactly what the option is for — skipping the browser's default scroll-into-view behavior. It's now a standing rule for this component going forward: even though only the OTP grid calls .focus() among these nine demos, it does it three times, and all three now carry preventScroll: true. The zip behind the download password zkv9xjxt carries every one of these calls already fixed, so pasting the code doesn't reintroduce the jump.

Accessibility (reduced-motion)

All nine turn off their decorative loops under prefers-reduced-motion: reduce — the permission modal's icon bounce, the captcha badge's spring overshoot, the OTP border flash, the security card's glow cycle, the recovery picker's auto-cycle highlight, the wizard's slide transition, the linked-accounts knob demo loop, and the sign-out card's shake — while every real state change still works instantly on click. The permission modal (01) is marked as a modal dialog and closes on Escape, the captcha checkbox (02) and toggle switches (07) expose their checked state to screen readers through the native checkbox itself, the timeout banner (08) announces its countdown through role="alert" so a screen reader hears it without scanning, and the wizard (06) keeps its progress visible with active-state dots on each step, though the step change itself isn't separately announced to screen readers.

These patterns pair well with the login and signup cards a person sees just before reaching any of these verification screens. More on what this site ships is on the about page.

FAQ

Does this include real ID verification or OTP logic?

No. These are screen UI and animation patterns only — the OTP grid accepts and formats digits, but nothing here calls a real SMS provider or checks a code against a server. Wiring up actual verification, session, or identity logic is up to your own backend.

Can I use just one of these nine instead of the whole set?

Yes. Each demo centers a single screen on its own page, and the zip ships that one screen's HTML, SCSS, and JS separately, so dropping in just the OTP grid or just the timeout banner works fine on its own.

Does the React version handle the OTP paste and countdown the same way?

Partially. The countdown timer runs through useEffect and useState in the React version and ticks the same way as the vanilla timer, but the OTP paste handler there works through useState and refs directly rather than an effect, and neither component takes duration, easing, or color as props — for those, the vanilla SCSS demo tokens are still the source of truth.

Enter the archive password

The password is inside this article. You will find it as you read.