نماذج
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 & 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 » وافتحه في المتصفح.
أكواد أخرى
عرض كل الأكوادتريد هذه التأثيرات في موقعك؟
ننجز موقعك حسب الطلب، سريعًا وبلغتين، مع الحركات التي تصنع الفرق.
اطلب عرض سعر