In this tutorial, we’re going to walk through the process of making an interactive input field for SMS verification codes. This feature, commonly used in two-factor authentication systems, can be a great addition to your web projects. We’ll keep things straightforward and use basic HTML and CSS.
Step 1: Setting Up the HTML Structure
Start by laying out the basic HTML structure. This includes a container for your input fields and necessary buttons for animation control. Make sure to keep the class names consistent for the CSS to work correctly.
<div> <div class="main"> <p class="label">Enter code from SMS</p> <!-- SMS Code input --> <div class="fieldset"> <! –– container should be fieldset element but there is bug in Chromium https://bugs.chromium.org/p/chromium/issues/detail?id=262679 ––> <label class="box"><input class="field" type="text" placeholder="•" /></label> <label class="box"><input class="field" type="text" placeholder="•" /></label> <label class="box"><input class="field" type="text" placeholder="•" /></label> <label class="box"><input class="field" type="text" placeholder="•" /></label> <label class="box"><input class="field" type="text" placeholder="•" /></label> </div> <!-- End SMS Code input --> </div> <div class="animation-controls"> <p class="label is-muted">States of animation</p> <div class="animation-controls__content"> <button class="btn success-btn" type="button">Success</button> <button class="btn failure-btn" type="button">Failure</button> <button class="btn reset-btn" type="button">Reset</button> </div> </div> <div class="settings-controls"> <p class="label is-muted">Animation duration settings (ms)</p> <div class="settings-controls__content"> <div> <label class="settings-controls__label" for="step-1">Step 1:</label> <input id="step-1" name="step-1" class="settings-controls__input" data-step="1" type="text" placeholder="Value" value="450"> </div> <div> <label class="settings-controls__label" for="step-2">Step 2:</label> <input id="step-2" name="step-2" class="settings-controls__input" data-step="2" type="text" placeholder="Value" value="300"> </div> <div> <label class="settings-controls__label" for="step-3">Step 3:</label> <input id="step-3" name="step-3" class="settings-controls__input" data-step="3" type="text" placeholder="Value" value="450"> </div> </div> </div> </div>
Step2: Styling with CSS
Next, incorporate the CSS to style your input field. This will include definitions for colors, border-radius, and other visual elements. The CSS code will also handle the responsiveness of the input field, ensuring it looks good on any device.
:root { /* colors palette */ --placeholder-color: hsl(240, 54%, 87%); --bg-color: hsl(240, 54%, 97%); --focus-color: hsla(240, 54%, 61%, 0.6); --shadow-color: hsla(240, 54%, 61%, 0.2); --text-color: hsl(0, 0%, 20%); --text-color-inversed: hsl(0, 0%, 95%); --success-color: hsl(145, 63%, 42%); --success-color-desaturated: hsl(145, 0%, 42%); --failure-color: hsl(0, 79%, 63%); /* border-radius */ --border-radius: 6px; /* z-index */ --z-index-xs: 1; --z-index-sm: 10; --z-index-md: 100; /* easing */ --easing: cubic-bezier(0.25, 0.01, 0.25, 1); /* transition durations */ --transition-duration-step-1: 450ms; --transition-duration-step-2: 300ms; --transition-duration-step-3: 300ms; /* transition delays */ --transition-delay-step-2: calc(var(--transition-duration-step-1)); --transition-delay-step-3: calc( var(--transition-duration-step-1) + var(--transition-duration-step-2) ); /* transition properties */ --transition-step-1: var(--transition-duration-step-1) var(--easing); --transition-step-2: var(--transition-duration-step-2) var(--easing) var(--transition-delay-step-2); --transition-step-3: var(--transition-duration-step-3) var(--easing) var(--transition-delay-step-3); } /* General styles */ *, *::after, *::before { box-sizing: border-box; font-family: Helvetica Neue; } body { display: flex; justify-content: center; align-items: center; height: 100vh; width: 100vw; margin: 0; } .main { display: grid; justify-content: center; } .label { font-size: 14px; line-height: 15px; text-align: center; letter-spacing: 0.01em; color: var(--text-color); } .label.is-muted { color: #828282; } .animation-controls { margin-top: 55px; } .animation-controls__content { display: grid; grid-auto-flow: column; justify-content: center; gap: 6px; } .btn { min-width: 90px; padding: 9px 0; border: 0; border-radius: 6px; font-size: 14px; line-height: 15px; letter-spacing: 0.01em; font-weight: bold; cursor: pointer; transition: opacity 150ms ease-in; } .btn:hover, .btn:focus { opacity: 0.7; } .btn:active { position: relative; top: 1px; } .btn.success-btn { color: #219653; background-color: #d3eadd; } .btn.failure-btn { color: #eb5757; background-color: #ffe9e0; } .btn.reset-btn { color: #6666d1; background-color: #f2f2f2; } .settings-controls { margin-top: 130px; } .settings-controls__content { display: grid; justify-content: center; gap: 18px; } .settings-controls__input { width: 67px; height: 33px; border: 0; border-radius: 6px; font-size: 14px; line-height: 15px; font-weight: bold; letter-spacing: 0.01em; color: #828282; background-color: #f2f2f2; text-align: center; } .settings-controls__label { font-size: 14px; line-height: 15px; letter-spacing: 0.01em; font-weight: bold; color: #828282; } @media (min-width: 768px) { .settings-controls__content { grid-auto-flow: column; } } /* SMS Code input styles */ /* base styles */ .fieldset { position: relative; display: grid; grid-auto-flow: column; justify-content: center; column-gap: 12px; border-radius: 6px; overflow: hidden; will-change: transform; /* make shadows of inner elements visible */ padding: 6px; margin: -6px; } .fieldset::before { content: ""; position: absolute; top: 0; left: 0; height: 100%; width: 100%; transform: translateX(-100%); } .box { position: relative; display: flex; align-items: center; justify-content: center; height: 42px; width: 42px; border-radius: var(--border-radius); box-shadow: 0 0 6px 1px var(--shadow-color); overflow: hidden; will-change: transform; } .box:focus-within { box-shadow: 0 0 6px 1px var(--shadow-color), 0 0 0 2px var(--focus-color); } .box::before, .box::after { content: ""; position: absolute; height: 100%; width: 100%; top: 0; left: 0; border-radius: var(--border-radius); overflow: hidden; } .box::before { background: var(--bg-color); z-index: var(--z-index-xs); transition: background-color var(--transition-step-1); } .box::after { transform: translateY(100%); background-color: var(--success-color-desaturated); opacity: 0; z-index: var(--z-index-sm); transition: transform var(--transition-step-1), opacity var(--transition-step-1), background-color var(--transition-step-1); } .field { position: relative; border: 0; outline: 0; font-size: 25.21px; line-height: 42px; color: var(--text-color); background-color: transparent; text-align: center; z-index: var(--z-index-md); } .field::placeholder { color: var(--placeholder-color); } /* animate-success styles */ .animate-success.fieldset { padding: 0; margin: 0; } .animate-success.fieldset::before { background-color: var(--success-color); transform: translateX(0); transition: transform var(--transition-step-2); } .animate-success > .box { box-shadow: none; transition: transform var(--transition-step-3); } .animate-success > .box:nth-child(1) { transform: translateX(24px); } .animate-success > .box:nth-child(2) { transform: translateX(12px); } .animate-success > .box:nth-child(4) { transform: translateX(-12px); } .animate-success > .box:nth-child(5) { transform: translateX(-24px); } .animate-success > .box::before { background-color: transparent; } .animate-success > .box::after { background-color: var(--success-color); transform: translateY(0); opacity: 1; } .animate-success > .box > .field, .animate-success > .box > .field::placeholder { color: var(--text-color-inversed); transition: color var(--transition-step-1); } /* animate-failure styles */ .animate-failure.fieldset { animation-name: shaking; animation-duration: var(--transition-duration-step-2); animation-timing-function: var(--easing); animation-delay: var(--transition-delay-step-2); } .animate-failure > .box::before { background-color: transparent; } .animate-failure > .box::after { background-color: var(--failure-color); transform: translateY(0); opacity: 1; } .animate-failure > .box > .field, .animate-failure > .box > .field::placeholder { color: var(--text-color-inversed); transition: color var(--transition-step-1); } @keyframes shaking { 0%, 100% { transform: translateX(0); } 25%, 75% { transform: translateX(10px); } 50% { transform: translateX(-10px); } } /* End SMS Code input styles */
Step3: JavaScript for Interaction:
Although our focus is on HTML and CSS, a small amount of JavaScript is necessary to manage the input behavior and animation triggers. This script will handle user inputs and activate the success or failure animations accordingly.
const root = document.documentElement; function getCustomPropertyValue(name) { const styles = getComputedStyle(root); return styles.getPropertyValue(name); } /* SMS Code input logic primitive implementation of multi-input Disclaimer: this «pen» was made for presentational pruposes. It's not a production-ready solution, because it lacks of many best UX and a11y practices. Let it inspire you and I hope you will enjoy it :) */ const fieldset = document.querySelector(".fieldset"); const fields = document.querySelectorAll(".field"); const boxes = document.querySelectorAll(".box"); function handleInputField({ target }) { const value = target.value.slice(0, 1); target.value = value; const step = value ? 1 : -1; const fieldIndex = [...fields].findIndex((field) => field === target); const focusToIndex = fieldIndex + step; if (focusToIndex < 0 || focusToIndex >= fields.length) return; fields[focusToIndex].focus(); } fields.forEach((field) => { field.addEventListener("input", handleInputField); }); /* End SMS Code input logic */ // Controls const successBtn = document.querySelector(".success-btn"); const failureBtn = document.querySelector(".failure-btn"); const resetBtn = document.querySelector(".reset-btn"); successBtn.addEventListener("click", (event) => { fieldset.classList.add("animate-success"); }); resetBtn.addEventListener("click", (event) => { fieldset.classList.remove("animate-failure"); fieldset.classList.remove("animate-success"); }); failureBtn.addEventListener("click", (event) => { function getDelay() { const firstStepDuration = getCustomPropertyValue( "--transition-duration-step-1" ); const secondStepDuration = getCustomPropertyValue( "--transition-duration-step-2" ); return parseInt(firstStepDuration) + parseInt(secondStepDuration); } function animateFailure() { fieldset.classList.add("animate-failure"); const delay = getDelay(); setTimeout(() => { fieldset.classList.remove("animate-failure"); }, delay); } if (fieldset.classList.contains("animate-success")) { fieldset.classList.remove("animate-success"); const delay = parseInt(getCustomPropertyValue("--transition-duration-step-1")) setTimeout(() => { animateFailure(); }, delay) return; } animateFailure(); }); const inputs = document.querySelectorAll(".settings-controls__input"); function setAnimationDuration({ target }) { const { value, dataset: { step } } = target; const safeValue = parseInt(value); const propertyValue = Number.isNaN(safeValue) ? null : safeValue + "ms"; root.style.setProperty(`--transition-duration-step-${step}`, propertyValue); } inputs.forEach((node) => { node.addEventListener("input", setAnimationDuration); });
Creating a Dynamic SMS Verification Code Input Field Using HTML CSS DEMO
With these steps, you’ve added a visually engaging SMS OTP input field to your web page. This feature not only enhances the user experience but also provides an added layer of interaction to your site.