الانتقال إلى المحتوى

نماذج

OTP Verification V5 — Orbite animée

Vérification OTP avec cases qui tournent en orbite, fusionnent puis se valident avec un anneau vert animé. HTML, CSS et JavaScript, prête à copier.

0 مشاهدة · 0 نسخة

مشاركة

النتيجة

ملء الشاشة
<div class="otp-card" id="otpCard">
  <span class="handle"></span>
  <div class="glow"></div>

  <div class="heading" id="otpHeading">
    <h1 class="title" id="otpTitle">Let's verify your number</h1>
    <p class="subtitle" id="otpSubtitle">We've sent a 4-digit code to your phone.<br>It'll auto-verify once entered.</p>
  </div>

  <div class="stage">
    <span class="orbit-path"></span>

    <div class="otp-inputs" id="otpInputs">
      <div class="cell"><input class="otp-box" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" aria-label="Digit 1"></div>
      <div class="cell"><input class="otp-box" type="text" inputmode="numeric" maxlength="1" aria-label="Digit 2"></div>
      <div class="cell"><input class="otp-box" type="text" inputmode="numeric" maxlength="1" aria-label="Digit 3"></div>
      <div class="cell"><input class="otp-box" type="text" inputmode="numeric" maxlength="1" aria-label="Digit 4"></div>
    </div>

    <div class="done" aria-hidden="true">
      <span class="ring"></span>
      <span class="sparks"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
      <span class="check-box">
        <svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>
      </span>
    </div>
  </div>

  <div class="footer">
    <p class="resend">
      Didn't receive the code?
      <button type="button" id="otpResend">Resend</button>
    </p>
    <p class="secured">
      Verified &amp; Secured
      <svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2.5" /><path d="M8.5 11V8a3.5 3.5 0 0 1 7 0v3" /></svg>
    </p>
  </div>

  <button type="button" class="replay" id="otpReplay">Try again</button>
</div>
* { box-sizing: border-box; }

@property --a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

body {
  min-height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
  background: #000;
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  color: #fff;
}

/* ---------- Carte ---------- */
.otp-card {
  position: relative;
  width: min(360px, 92vw);
  padding: 40px 24px 28px;
  border-radius: 30px;
  background: linear-gradient(180deg, #1b1b1d 0%, #111113 100%);
  border: 1px solid rgba(255, 255, 255, .06);
  box-shadow: 0 30px 80px rgba(0, 0, 0, .65), inset 0 1px 0 rgba(255, 255, 255, .05);
  text-align: center;
  overflow: hidden;
  animation: card-in .7s cubic-bezier(.2, .9, .3, 1.15) both;
}

.handle {
  position: absolute;
  top: 12px;
  left: 50%;
  width: 38px;
  height: 4px;
  border-radius: 99px;
  background: rgba(255, 255, 255, .14);
  transform: translateX(-50%);
}

.glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 58%, rgba(34, 197, 94, .26), transparent 60%);
  opacity: 0;
  transition: opacity 1s ease;
  pointer-events: none;
}
.otp-card.is-done .glow { opacity: 1; }

/* ---------- Textes ---------- */
.heading { position: relative; z-index: 2; }
.title { margin: 8px 0 10px; font-size: 21px; font-weight: 700; letter-spacing: -.01em; }
.subtitle { margin: 0; font-size: 12.5px; line-height: 1.55; color: #6b6b70; white-space: pre-line; }
.title, .subtitle { transition: opacity .35s ease, transform .35s ease, filter .35s ease; }
.heading.is-swapping .title,
.heading.is-swapping .subtitle { opacity: 0; transform: translateY(-6px); filter: blur(5px); }

/* ---------- Zone d'animation ---------- */
.stage {
  position: relative;
  height: 170px;
  margin: 4px 0 0;
  display: grid;
  place-items: center;
}

/* Cercle pointillé suivi par les cases pendant l'orbite */
.orbit-path {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 130px;
  height: 130px;
  margin: -65px 0 0 -65px;
  border-radius: 50%;
  border: 1.5px dotted rgba(255, 255, 255, .12);
  opacity: 0;
  transform: scale(.6);
  transition: opacity .4s ease, transform .5s ease;
}
.otp-card.is-orbiting .orbit-path { opacity: 1; transform: scale(1); }

.otp-inputs {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 12px;
}

/* ---------- Cases ---------- */
.cell {
  position: relative;
  width: 52px;
  height: 56px;
  border-radius: 13px;
  animation: box-in .5s cubic-bezier(.2, .9, .3, 1.3) both;
}
.cell:nth-child(1) { animation-delay: .15s; }
.cell:nth-child(2) { animation-delay: .22s; }
.cell:nth-child(3) { animation-delay: .29s; }
.cell:nth-child(4) { animation-delay: .36s; }

.otp-box {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  border: 1.5px solid #333338;
  background: #19191c;
  color: #fff;
  font-family: inherit;
  font-size: 22px;
  font-weight: 600;
  line-height: 1;
  text-align: center;
  caret-color: #fb923c;
  outline: none;
  box-shadow: 0 8px 18px rgba(0, 0, 0, .35);
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease, color .25s ease;
}

.cell:focus-within .otp-box {
  border-color: #f97316;
  background: #1d1714;
  box-shadow: 0 0 0 4px rgba(249, 115, 22, .13), 0 0 22px rgba(249, 115, 22, .25);
}
.otp-box.is-filled { border-color: #45454b; }
.cell.pop { animation: pop .28s cubic-bezier(.3, 1.6, .5, 1); }

/* Lumière orange qui fait le tour du contour (« comète ») */
.cell::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(from var(--a), #fb923c 0deg, #f97316 55deg, transparent 75deg 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease;
}
.otp-card.is-charging .cell::after { opacity: 1; animation: comet 1.1s linear infinite; }
.otp-card.is-charging .cell { filter: drop-shadow(0 6px 10px rgba(249, 115, 22, .22)); }
.otp-card.is-merged .otp-box { color: transparent; border-color: #f97316; }

.otp-inputs.shake { animation: shake .4s ease; }

/* ---------- Succès ---------- */
.done {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.ring {
  position: absolute;
  width: 136px;
  height: 136px;
  border-radius: 50%;
  border: 1.5px solid #22c55e;
  box-shadow: 0 0 24px rgba(34, 197, 94, .25), inset 0 0 24px rgba(34, 197, 94, .12);
  opacity: 0;
  transform: scale(.35);
}
.otp-card.is-done .ring { animation: ring 1.6s cubic-bezier(.2, .8, .3, 1) forwards; }

.sparks { position: absolute; width: 0; height: 0; }
.sparks i {
  position: absolute;
  width: 4px;
  height: 4px;
  margin: -2px;
  border-radius: 50%;
  background: #4ade80;
  box-shadow: 0 0 8px #22c55e;
  opacity: 0;
  transform: rotate(calc(var(--i) * 45deg)) translateY(0);
}
.sparks i:nth-child(1) { --i: 0; } .sparks i:nth-child(2) { --i: 1; }
.sparks i:nth-child(3) { --i: 2; } .sparks i:nth-child(4) { --i: 3; }
.sparks i:nth-child(5) { --i: 4; } .sparks i:nth-child(6) { --i: 5; }
.sparks i:nth-child(7) { --i: 6; } .sparks i:nth-child(8) { --i: 7; }
.otp-card.is-done .sparks i { animation: spark 1.3s ease-out .1s forwards; }

.check-box {
  position: relative;
  width: 52px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 13px;
  border: 1.5px solid #22c55e;
  background: #131a15;
  box-shadow: 0 0 0 5px rgba(34, 197, 94, .1), 0 0 30px rgba(34, 197, 94, .4);
  opacity: 0;
  transform: scale(.6);
}
.check-box svg {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: #fff;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
}
.otp-card.is-done .check-box { animation: check-in .55s cubic-bezier(.2, 1.5, .4, 1) forwards; }
.otp-card.is-done .check-box svg { animation: draw .45s ease .45s forwards; }

/* ---------- Bas de carte ---------- */
.footer { position: relative; height: 22px; margin-top: 4px; }
.resend, .secured {
  position: absolute;
  inset: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-size: 12.5px;
  transition: opacity .4s ease, transform .4s ease;
}
.resend { color: #6b6b70; }
.resend button {
  padding: 0;
  border: 0;
  background: none;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.resend button:disabled { color: #55555a; cursor: default; }
.otp-card.is-charging .resend,
.otp-card.is-done .resend { opacity: 0; transform: translateY(6px); pointer-events: none; }

.secured { color: #22c55e; font-weight: 600; opacity: 0; transform: translateY(6px); }
.secured svg { width: 14px; height: 14px; fill: none; stroke: #f97316; stroke-width: 2.2; stroke-linecap: round; }
.otp-card.is-done .secured { opacity: 1; transform: none; transition-delay: .5s; }

.replay {
  position: absolute;
  right: 16px;
  bottom: 12px;
  padding: 4px 8px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: #55555a;
  font: 600 11px/1 system-ui, sans-serif;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s ease 1.6s, color .2s;
}
.replay:hover { color: #fff; }
.otp-card.is-done .replay { opacity: 1; pointer-events: auto; }

/* ---------- Animations ---------- */
@keyframes card-in { from { opacity: 0; transform: translateY(24px) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes box-in { from { opacity: 0; transform: translateY(14px) scale(.85); } to { opacity: 1; transform: none; } }
@keyframes pop { 45% { transform: scale(1.14); } }
@keyframes shake { 20%, 60% { transform: translateX(-7px); } 40%, 80% { transform: translateX(7px); } }
@keyframes comet { to { --a: 360deg; } }
@keyframes ring {
  0% { opacity: 0; transform: scale(.35); }
  25% { opacity: 1; }
  60% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.5); }
}
@keyframes spark {
  0% { opacity: 0; transform: rotate(calc(var(--i) * 45deg)) translateY(-26px) scale(.4); }
  30% { opacity: 1; }
  100% { opacity: 0; transform: rotate(calc(var(--i) * 45deg)) translateY(-62px) scale(1); }
}
@keyframes check-in { to { opacity: 1; transform: scale(1); } }
@keyframes draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
// OTP Verification V5 — orbite, fusion et anneau de succès (vanilla JS, Web Animations API)
const card = document.getElementById('otpCard');
const wrap = document.getElementById('otpInputs');
const cells = [...wrap.querySelectorAll('.cell')];
const boxes = cells.map((c) => c.querySelector('.otp-box'));
const heading = document.getElementById('otpHeading');
const title = document.getElementById('otpTitle');
const subtitle = document.getElementById('otpSubtitle');
const resendBtn = document.getElementById('otpResend');
const replayBtn = document.getElementById('otpReplay');

const LENGTH = boxes.length;
const RADIUS = 64;             // rayon du losange / de l'orbite (px)
const TEXT_START = ["Let's verify your number", "We've sent a 4-digit code to your phone.\nIt'll auto-verify once entered."];
const TEXT_DONE = ['Verified successfully', 'Your phone number has been verified.'];

let busy = false;
let timer = null;

const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const code = () => boxes.map((b) => b.value).join('');

// ---------- Saisie ----------
function setDigit(i, value) {
  boxes[i].value = value;
  boxes[i].classList.toggle('is-filled', value !== '');
  if (value) {
    cells[i].classList.remove('pop');
    void cells[i].offsetWidth;
    cells[i].classList.add('pop');
  }
}

function focusBox(i) {
  const box = boxes[Math.max(0, Math.min(LENGTH - 1, i))];
  box.focus();
  box.select();
}

boxes.forEach((box, i) => {
  box.addEventListener('input', () => {
    const digits = box.value.replace(/\D/g, '');
    if (!digits) { setDigit(i, ''); return; }
    [...digits].slice(0, LENGTH - i).forEach((d, k) => setDigit(i + k, d));
    if (code().length === LENGTH) verify(); else focusBox(Math.min(i + digits.length, LENGTH - 1));
  });

  box.addEventListener('keydown', (e) => {
    if (busy) { e.preventDefault(); return; }
    if (e.key === 'Backspace' && !box.value && i > 0) {
      e.preventDefault(); setDigit(i - 1, ''); focusBox(i - 1);
    } else if (e.key === 'ArrowLeft') {
      e.preventDefault(); focusBox(i - 1);
    } else if (e.key === 'ArrowRight') {
      e.preventDefault(); focusBox(i + 1);
    } else if (e.key === 'Enter' && code().length < LENGTH) {
      wrap.classList.remove('shake'); void wrap.offsetWidth; wrap.classList.add('shake');
    }
  });

  box.addEventListener('focus', () => box.select());

  box.addEventListener('paste', (e) => {
    e.preventDefault();
    const digits = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, LENGTH);
    digits.split('').forEach((d, k) => setDigit(k, d));
    if (code().length === LENGTH) verify(); else focusBox(digits.length);
  });
});

function swapText([t, s]) {
  heading.classList.add('is-swapping');
  setTimeout(() => {
    title.textContent = t;
    subtitle.textContent = s;
    heading.classList.remove('is-swapping');
  }, 350);
}

// ---------- Animation de validation ----------
async function verify() {
  if (busy || code().length !== LENGTH) return;
  busy = true;
  boxes.forEach((b) => { b.readOnly = true; b.blur(); });

  // 1. La lumière orange fait le tour de chaque case.
  card.classList.add('is-charging');
  await wait(700);

  // 2. Les cases s'écartent en losange (haut, droite, bas, gauche) autour du centre.
  const center = wrap.offsetWidth / 2;
  const natural = cells.map((c) => c.offsetLeft + c.offsetWidth / 2 - center);
  const spots = [[0, -RADIUS], [RADIUS, 0], [0, RADIUS], [-RADIUS, 0]];
  const toDiamond = cells.map((c, i) => c.animate(
    [{ transform: 'translate(0, 0)' }, { transform: `translate(${spots[i][0] - natural[i]}px, ${spots[i][1]}px)` }],
    { duration: 600, easing: 'cubic-bezier(.5, 0, .2, 1.25)', fill: 'forwards' },
  ));
  card.classList.add('is-orbiting');
  await Promise.all(toDiamond.map((a) => a.finished));

  // 3. Orbite : tout le groupe tourne d'un tour complet.
  await wrap.animate(
    [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
    { duration: 1400, easing: 'cubic-bezier(.65, 0, .35, 1)', fill: 'forwards' },
  ).finished;

  // 4. Fusion : les cases se rejoignent au centre et deviennent une seule case vide.
  card.classList.remove('is-orbiting');
  card.classList.add('is-merged'); // les chiffres s'effacent pendant le rapprochement
  await Promise.all(cells.map((c, i) => c.animate(
    [{ transform: `translate(${spots[i][0] - natural[i]}px, ${spots[i][1]}px)` }, { transform: `translate(${-natural[i]}px, 0)` }],
    { duration: 450, easing: 'cubic-bezier(.6, 0, .3, 1)', fill: 'forwards' },
  ).finished));
  cells.slice(1).forEach((c) => c.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' }));
  await wait(350);

  // 5. Succès : la case devient verte, l'anneau s'ouvre, la coche se dessine.
  card.classList.remove('is-charging');
  wrap.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 180, fill: 'forwards' });
  card.classList.add('is-done');
  swapText(TEXT_DONE);
  // Ici : appelez votre API pour vérifier vraiment le code.
}

// ---------- Remise à zéro ----------
function reset() {
  [wrap, ...cells].forEach((el) => el.getAnimations().forEach((a) => a.cancel()));
  card.classList.remove('is-charging', 'is-orbiting', 'is-merged', 'is-done');
  boxes.forEach((b, i) => { b.readOnly = false; setDigit(i, ''); });
  busy = false;
}

function startCountdown(seconds) {
  clearInterval(timer);
  let left = seconds;
  resendBtn.disabled = true;
  resendBtn.textContent = `Resend in ${left}s`;
  timer = setInterval(() => {
    left -= 1;
    if (left <= 0) { clearInterval(timer); resendBtn.disabled = false; resendBtn.textContent = 'Resend'; }
    else resendBtn.textContent = `Resend in ${left}s`;
  }, 1000);
}

resendBtn.addEventListener('click', () => { reset(); focusBox(0); startCountdown(30); });

replayBtn.addEventListener('click', () => {
  reset();
  swapText(TEXT_START);
  setTimeout(() => focusBox(0), 400);
});

setTimeout(() => focusBox(0), 500);

انسخ الملف الكامل، والصقه في ملف « index.html » وافتحه في المتصفح.

أكواد أخرى

عرض كل الأكواد

تريد هذه التأثيرات في موقعك؟

ننجز موقعك حسب الطلب، سريعًا وبلغتين، مع الحركات التي تصنع الفرق.

اطلب عرض سعر