From 6bafa481e0aaba1b880b8d460b023cf9bc2e0054 Mon Sep 17 00:00:00 2001 From: prakash meena Date: Thu, 4 Jun 2026 17:50:46 +0530 Subject: [PATCH] feat: add OTP Input Component (#1200) --- submissions/examples/otp-input/README.md | 29 +++++ submissions/examples/otp-input/demo.html | 95 ++++++++++++++++ submissions/examples/otp-input/style.css | 133 +++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 submissions/examples/otp-input/README.md create mode 100644 submissions/examples/otp-input/demo.html create mode 100644 submissions/examples/otp-input/style.css diff --git a/submissions/examples/otp-input/README.md b/submissions/examples/otp-input/README.md new file mode 100644 index 00000000..796491ed --- /dev/null +++ b/submissions/examples/otp-input/README.md @@ -0,0 +1,29 @@ +# OTP Input Component + +A row of 6 individual digit input boxes for entering one-time passwords or verification codes. Features auto-focus to next box on input, backspace to go back, paste support, and animated states (pulse on empty, pop on fill, shake on error, staggered success reveal). Includes inline JavaScript for input handling. + +## Classes + +| Class | Description | +|---|---| +| `ease-otp-input` | Flex container for the digit boxes | +| `ease-otp-box` | Individual digit input | +| `ease-otp-box--active` | Currently focused box | +| `ease-otp-box--filled` | Box with a digit entered | +| `ease-otp-input--error` | Error state (red shake on all boxes) | +| `ease-otp-input--success` | Success state (staggered green pop) | + +## Usage + +```html +
+ + +
+``` + +See `demo.html` for the full JS setup (auto-focus, backspace, paste, Enter to validate). + +## Why it fits EaseMotion CSS + +Four animated states with `ease-` prefixed keyframes: `ease-kf-otp-pulse` (empty waiting), `ease-kf-otp-pop` (digit entry), `ease-kf-otp-shake` (error), and `ease-kf-otp-success-pop` (staggered success). Smooth border and transform transitions. Respects `prefers-reduced-motion`. diff --git a/submissions/examples/otp-input/demo.html b/submissions/examples/otp-input/demo.html new file mode 100644 index 00000000..ec91aa0b --- /dev/null +++ b/submissions/examples/otp-input/demo.html @@ -0,0 +1,95 @@ + + + + + + OTP Input — EaseMotion CSS + + + + +
+ + + + + + +
+ + + + + diff --git a/submissions/examples/otp-input/style.css b/submissions/examples/otp-input/style.css new file mode 100644 index 00000000..2755c0c8 --- /dev/null +++ b/submissions/examples/otp-input/style.css @@ -0,0 +1,133 @@ +/* ============================================================ + EaseMotion CSS — OTP Input Component + Issue #1200 + ============================================================ */ + +/* ── Container ────────────────────────────────────────────── */ + +.ease-otp-input { + display: flex; + gap: 12px; + justify-content: center; + align-items: center; + max-width: 480px; + margin: 2rem auto; +} + +/* ── Box ──────────────────────────────────────────────────── */ + +.ease-otp-box { + width: 52px; + height: 56px; + border: 2px solid rgba(255, 255, 255, 0.15); + border-radius: var(--ease-radius-md, 0.625rem); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 600; + font-family: var(--ease-font-mono, ui-monospace, "SF Mono", monospace); + text-align: center; + background: rgba(255, 255, 255, 0.03); + color: rgba(255, 255, 255, 0.9); + caret-color: #7c6cff; + outline: none; + transition: + border-color var(--ease-speed-fast, 150ms) cubic-bezier(0.4, 0, 0.2, 1), + transform var(--ease-speed-fast, 150ms) cubic-bezier(0.4, 0, 0.2, 1), + box-shadow var(--ease-speed-fast, 150ms) cubic-bezier(0.4, 0, 0.2, 1), + background var(--ease-speed-fast, 150ms) cubic-bezier(0.4, 0, 0.2, 1); + animation: ease-kf-otp-pulse 2.5s ease-in-out infinite; +} + +.ease-otp-box:focus { + border-color: #7c6cff; + box-shadow: 0 0 0 3px rgba(124, 108, 255, 0.15); + animation: none; +} + +.ease-otp-box--active { + border-color: #7c6cff; + animation: none; +} + +/* ── Filled state ─────────────────────────────────────────── */ + +.ease-otp-box--filled { + animation: ease-kf-otp-pop 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + border-color: #7c6cff; +} + +/* ── Error state ──────────────────────────────────────────── */ + +.ease-otp-input--error .ease-otp-box { + border-color: #ef4444; + animation: ease-kf-otp-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97); + color: #ef4444; +} + +.ease-otp-input--error .ease-otp-box:focus { + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15); +} + +/* ── Success state ────────────────────────────────────────── */ + +.ease-otp-input--success .ease-otp-box { + border-color: #22c55e; + animation: ease-kf-otp-success-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; + background: rgba(34, 197, 94, 0.1); + color: #22c55e; +} + +.ease-otp-input--success .ease-otp-box:nth-child(2) { animation-delay: 0.04s; } +.ease-otp-input--success .ease-otp-box:nth-child(3) { animation-delay: 0.08s; } +.ease-otp-input--success .ease-otp-box:nth-child(4) { animation-delay: 0.12s; } +.ease-otp-input--success .ease-otp-box:nth-child(5) { animation-delay: 0.16s; } +.ease-otp-input--success .ease-otp-box:nth-child(6) { animation-delay: 0.20s; } + +/* ── Keyframes ────────────────────────────────────────────── */ + +@keyframes ease-kf-otp-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +@keyframes ease-kf-otp-pop { + 0% { transform: scale(0.85); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +@keyframes ease-kf-otp-shake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} + +@keyframes ease-kf-otp-success-pop { + 0% { transform: scale(1); background: rgba(34, 197, 94, 0); } + 50% { transform: scale(1.15); } + 100% { transform: scale(1); background: rgba(34, 197, 94, 0.1); } +} + +/* ── Reduced motion ──────────────────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + .ease-otp-box { + animation: none; + } + .ease-otp-box:focus { + animation: none; + } + .ease-otp-box--filled { + animation: none; + } + .ease-otp-input--error .ease-otp-box { + animation: none; + } + .ease-otp-input--success .ease-otp-box { + animation: none; + } +}