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; + } +}