Stepper
Stepper
Overview
A stepper widget is an user interface component that guides users through a series of steps or stages in a process, such as filling out a form or completing a workflow. It visually represents progress and allows users to navigate between steps, typically using numbered labels or icons, often with forward/backward navigation controls.
Turn on a screen reader to experience this example in action.
Attribute/Option | Description |
---|---|
allowStepChange |
This is a required option that needs to be passed as a function which decides if the transition between the steps should be allowed or not. The function should validate the current step's user inputs and return "true" to allow the transition. The function should return "false" to prevent the transition. |
processRequest |
This is a required option that needs to be passed as a function which performs the final processing of data. |
HTML Source Code
<table class="data">
<tr>
<th width="150px" >Attribute/Option</th>
<th>Description</th>
</tr>
<tr>
<td><code>allowStepChange</code></td>
<td>This is a required option that needs to be passed as a function which decides if the transition between the steps should be allowed or not. The function should validate the current step's user inputs and return "true" to allow the transition. The function should return "false" to prevent the transition.
</td>
</tr>
<tr>
<td><code>processRequest</code></td>
<td>This is a required option that needs to be passed as a function which performs the final processing of data.
</td>
</tr>
</table>
<br/><br/>
<div id="stepper1" >
<div class="stepper-steps-container" >
<div step-name="Owner" class="stepper-step" >
<form>
<div class="field" >
<label for="first-name" >First Name</label>
<input type="text" maxlength="30" id="first-name" />
</div>
<div class="field" >
<label for="last-name">Last Name</label>
<input type="text" maxlength="30" id="last-name" />
</div>
<div class="field" >
<label for="owner-age">Age</label>
<input type="number" min="18" max="100" inputmode="numeric" id="owner-age" />
</div>
</form>
</div>
<div step-name="Car" class="stepper-step" >
<form>
<div class="field" >
<label for="car-make" >Car Make</label>
<input type="text" maxlength="30" id="car-make" />
</div>
<div class="field" >
<label for="car-model">Car Model</label>
<input type="text" maxlength="30" id="car-model" />
</div>
<div class="field" >
<label for="car-age">Year of Manufacturing</label>
<input type="number" min="1980" max="2025" inputmode="numeric" id="car-age" />
</div>
</form>
</div>
<div step-name="Policy" class="stepper-step" >
<form>
<div class="field" >
<fieldset>
<legend>Policy Type</legend>
<input type="radio" name="policy-type" id="policy-type-comp" /><label for="policy-type-comp">Full Comprehensive</label>
<input type="radio" name="policy-type" id="policy-type-basic" /><label for="policy-type-basic" >Basic</label>
</fieldset>
</div>
<div class="field" >
<fieldset>
<legend id="policy-type" >Previous Claims(Last 5 years)</legend>
<input type="radio" name="prev-claims" id="prev-claims-yes" aria-describedby="policy-type"></input><label for="prev-claims-yes" >Yes</label>
<input type="radio" name="prev-claims" id="prev-claims-no" aria-describedby="policy-type" ></input><label for="prev-claims-no" >No</label>
</fieldset>
</div>
<div class="field" >
<label for="policy-duration">Duration</label>
<input type="number" min="1" max="5" inputmode="numeric" id="policy-duration" />
</div>
</form>
</div>
<div step-name="Payment" class="stepper-step" >
<form>
<div class="field" >
<label for="payment-frequency" >Payment Frequency</label>
<select id="payment-frequency">
<option selected>Yearly</option>
<option>Half-yearly</option>
<option>Monthly</option>
</select>
</div>
<div class="field" >
<label for="card-number" >Credit/Debit Card Number</label>
<input type="number" id="card-number" type="tel" inputmode="numeric" pattern="[0-9\s]{13,19}"
autocomplete="cc-number" maxlength="16"
placeholder="xxxx xxxx xxxx xxxx"/>
</div>
<div class="field" >
<label for="expiration-date">Expiration Date</label>
<input type="date" format="MM/YYYYY" id="expiration-date" />
</div>
<div class="field" >
<label for="card-cvv">CVV</label>
<input type="number" maxlength="3" id="card-cvv" />
</div>
</form>
</div>
</div>
</div>
JavaScript Source Code
const HORIZONTAL = 0;
const VERTICAL = 1;
class Stepper {
static PENDING_STATE = 1;
static ACTIVE_STATE = 2;
static COMPLETED_STATE = 3;
constructor(domNode, options) {
this.orientation = HORIZONTAL;
this.panelColor = "white";
var self = this;
this.currentStep = 0;
this.callback = {};
this.callback.allowStepChange = null;
this.callback.processRequest = null;
if ( typeof options != "object" ) options = {};
if( (typeof options["orientation"] === "number") && ( (options["orientation"] === HORIZONTAL) || (options["orientation"] === VERTICAL)) )
this.orientation = options["orientation"];
if(typeof options["panelColor"] === "string")
this.panelColor = options["panelColor"];
if( (typeof options["allowStepChange"] === "function") )
this.callback.allowStepChange = options["allowStepChange"];
if( this.callback.allowStepChange == null)
{
console.log(langText.errorNodeNotObject);
return;
}
if( (typeof options["processRequest"] === "function") )
this.callback.processRequest = options["processRequest"];
if( this.callback.processRequest == null)
{
console.log(langText.errorProcessRequest);
return;
}
if( (typeof domNode != "object" ) && (typeof domNode.nodeName === "undefined") )
{
console.log(langText.errorNodeNotObject);
return;
}
this.domNode = domNode;
this.stepsContainer = this.domNode.querySelector('.stepper-steps-container');
if(!this.stepsContainer)
{
console.log(langText.errorStepsContainer);
return;
}
this.steps = this.stepsContainer.querySelectorAll('.stepper-step');
this.stepList = document.createElement("ol");
this.stepList.classList.add("stepper-step-list");
this.steps.forEach(function(step, i){
var stepName = step.getAttribute("step-name");
if(stepName)
{
var stepButton = document.createElement("span");
stepButton.innerHTML = (i+1);
var stepDiv = document.createElement("li");
stepDiv.appendChild(stepButton);
var stepText = document.createElement("span");
stepText.innerHTML = stepName;
stepText.id = crypto.randomUUID();
stepText.setAttribute("align", "center");
stepDiv.appendChild(stepText);
stepDiv.style = "display: flex; flex-direction: column;";
self.stepList.appendChild(stepDiv);
stepButton.classList.add("stepper-step-list-item");
if( i < (self.steps.length-1) )
{
var connector = document.createElement("li");
connector.classList.add("stepper-connector");
self.stepList.appendChild(connector);
}
var focusElements = step.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
focusElements.forEach(function(element, j) {
element.setAttribute("aria-describedby", stepText.id);
});
}
if (i != 0 )
step.classList.add("hidden");
});
this.stepsContainer.remove();
this.domNode.appendChild(this.stepList);
this.domNode.appendChild(this.stepsContainer);
this.setStepAsActive();
this.buttonContainer = document.createElement("div");
var btnPrevious = document.createElement("button");
btnPrevious.innerHTML = langText.previous;
var btnNext = document.createElement("button");
btnNext.innerHTML = langText.next;
this.buttonContainer.appendChild(btnPrevious);
this.buttonContainer.appendChild(btnNext);
this.buttonContainer.classList.add("stepper-button-container");
btnPrevious.classList.add("stepper-button");
btnNext.classList.add("stepper-button");
btnNext.classList.add("stepper-primary");
this.domNode.appendChild(this.buttonContainer);
btnNext.addEventListener("click", this.showNextStep.bind(this));
btnPrevious.addEventListener("click", this.showPreviousStep.bind(this));
}
setStepAsActive()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
stepItems[this.currentStep].classList.add("stepper-step-list-item-active");
stepItems[this.currentStep].innerHTML = ( this.currentStep+1);
stepItems[this.currentStep].parentElement.setAttribute("aria-current", "true");
var focusElements = this.steps[this.currentStep].querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
if(focusElements.length > 0)
setTimeout(function(){
focusElements[0].focus();
},300);
}
removeStepAsActive()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
stepItems[this.currentStep].classList.remove("stepper-step-list-item-active");
stepItems[this.currentStep].parentElement.removeAttribute("aria-current");
}
highlightConnector()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
if(stepItems[this.currentStep].parentElement.previousSibling)
stepItems[this.currentStep].parentElement.previousSibling.classList.add("stepper-connector-highlight");
}
resetConnector()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
if(stepItems[this.currentStep].parentElement.previousSibling)
stepItems[this.currentStep].parentElement.previousSibling.classList.remove("stepper-connector-highlight");
}
markStepAsCompleted()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
stepItems[this.currentStep].classList.add("stepper-step-list-item-completed");
stepItems[this.currentStep].innerHTML = "<span class='fas fa-check'></span>";
}
markStepAsNotCompleted()
{
var stepItems = this.stepList.querySelectorAll(".stepper-step-list-item");
stepItems[this.currentStep].classList.remove("stepper-step-list-item-completed");
}
showPreviousStep()
{
if ( this.currentStep == 0 )
return;
this.removeStepAsActive();
this.resetConnector();
this.steps[this.currentStep].classList.add("hidden");
this.currentStep--;
this.markStepAsNotCompleted();
this.setStepAsActive();
this.steps[this.currentStep].classList.remove("hidden");
if( (this.currentStep ) == ( this.steps.length -2) )
this.buttonContainer.children[1].innerHTML= langText.next;
}
showNextStep()
{
if ( this.currentStep == ( this.steps.length - 1) )
{
// validate final step and call processRequest() for final processing...
if( this.callback.allowStepChange( this.currentStep, ( this.currentStep+1) ))
this.callback.processRequest();
return;
}
if( !this.callback.allowStepChange( this.currentStep, ( this.currentStep+1) ))
return;
this.removeStepAsActive();
this.markStepAsCompleted();
this.steps[this.currentStep].classList.add("hidden");
this.currentStep++;
this.setStepAsActive();
this.steps[this.currentStep].classList.remove("hidden");
this.highlightConnector();
if( (this.currentStep ) == ( this.steps.length -1) )
this.buttonContainer.children[1].innerHTML= langText.submit;
}
}
window.addEventListener('load', function () {
options = {};
//options.orientation = VERTICAL;
options.allowStepChange = validateStepchange;
options.processRequest = processSubmit;
fetch(translationFilePath+'/translation.json')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
langText = data;
stepper = new Stepper(document.getElementById("stepper1"), options);
})
.catch(error => {
console.error("Error fetching or parsing JSON:", error);
});
});
function validateStepchange(fromStep, toStep)
{
if (fromStep == 0 )
return validateOwnerDetails();
if (fromStep == 1 )
return validateCarDetails();
if (fromStep == 2 )
return validatePolicyDetails();
if (fromStep == 3 )
return validatePaymentDetails();
}
function validateOwnerDetails()
{
if(document.getElementById("first-name").value.trim() == "")
{
alert(langText.msgEnterFirstName);
document.getElementById("first-name").focus();
return false;
}
if(document.getElementById("last-name").value.trim() == "")
{
alert(langText.msgEnterLastName);
document.getElementById("last-name").focus();
return false;
}
if(document.getElementById("owner-age").value.trim() == "")
{
alert(langText.msgEnterOwnerAge);
document.getElementById("owner-age").focus();
return false;
}
var age = parseInt(document.getElementById("owner-age").value.trim());
if( (age == NaN) || !( age == document.getElementById("owner-age").value.trim() ) || ((age < 18) || (age > 100) ) )
{
alert(langText.msgOwnerAgeInvalid);
document.getElementById("owner-age").focus();
return false;
}
return true;
}
function validateCarDetails()
{
if(document.getElementById("car-make").value.trim() == "")
{
alert(langText.msgEnterCarMake);
document.getElementById("car-make").focus();
return false;
}
if(document.getElementById("car-model").value.trim() == "")
{
alert(langText.msgEnterCarModel);
document.getElementById("car-model").focus();
return false;
}
if(document.getElementById("car-age").value.trim() == "")
{
alert(langText.msgEnterCarYear);
document.getElementById("car-age").focus();
return false;
}
var age = parseInt(document.getElementById("car-age").value.trim());
if( (age == NaN) || !( age == document.getElementById("car-age").value.trim()) || ((age < 1980) || (age > 2025) ) )
{
alert(langText.msgcCarYearInvalid);
document.getElementById("car-age").focus();
return false;
}
return true;
}
function validatePolicyDetails()
{
if( !document.getElementById("policy-type-comp").checked && !document.getElementById("policy-type-basic").checked )
{
alert(langText.msgSelectPolicyType);
if( !document.getElementById("policy-type-comp").checked)
document.getElementById("policy-type-comp").focus();
else
document.getElementById("policy-type-basic").focus();
return false;
}
if( !document.getElementById("prev-claims-yes").checked && !document.getElementById("prev-claims-no").checked )
{
alert(langText.msgSelectPolicyClaims);
if( !document.getElementById("prev-claims-yes").checked)
document.getElementById("prev-claims-yes").focus();
else
document.getElementById("prev-claims-no").focus();
return false;
}
if(document.getElementById("policy-duration").value.trim() == "")
{
alert(langText.msgEnterPolicyDuration);
document.getElementById("policy-duration").focus();
return false;
}
var duration = parseInt(document.getElementById("policy-duration").value.trim());
if( (duration == NaN) || !( duration == document.getElementById("policy-duration").value.trim()) || ((duration < 1) || (duration > 5) ) )
{
alert(langText.msgPolicyDurationInvalid);
document.getElementById("policy-duration").focus();
return false;
}
return true;
}
function validatePaymentDetails()
{
if(document.getElementById("payment-frequency").selectedIndex == -1 )
{
alert(langText.msgPaymentFrequency);
document.getElementById("payment-frequency").focus();
return false;
}
if(document.getElementById("card-number").value.trim() == "")
{
alert(langText.msgEnterCardNumber);
document.getElementById("card-number").focus();
return false;
}
var cardNumber = parseInt(document.getElementById("card-number").value.trim());
if( (cardNumber == NaN) || !( cardNumber == document.getElementById("card-number").value.trim()) || ((cardNumber < 1000000000000000 ) || (cardNumber > 9999999999999999 ) ) )
{
alert(langText.msgCardNumberInvalid);
document.getElementById("card-number").focus();
return false;
}
if(document.getElementById("expiration-date").value.trim() == "")
{
alert(langText.msgEnterExpDate);
document.getElementById("expiration-date").focus();
return false;
}
if(document.getElementById("card-cvv").value.trim() == "")
{
alert(langText.msgEnterCVV);
document.getElementById("card-cvv").focus();
return false;
}
var cardCVV = parseInt(document.getElementById("card-cvv").value.trim());
if( (cardCVV == NaN) || !( cardCVV == document.getElementById("card-cvv").value.trim()) || ((cardCVV < 100 ) || (cardCVV > 999 ) ) )
{
alert(langText.msgCVVInvalid);
document.getElementById("card-cvv").focus();
return false;
}
return true;
}
function processSubmit()
{
alert(langText.msgDataProcessing);
}
CSS Source Code
.stepper-step-list{
margin-bottom: 15px;
display: flex;
align-items: center;
}
.stepper-step-list-item {
border-radius: 96px;
padding: 10px 16px;
margin: 0px 20px;
width: 8px;
height: 18.5px;
font-size: 16px;
background-color: #D4DDE0;
display: inline-flex;
border: none;
line-height: normal;
}
.stepper-step-list-item-active {
background-color: transparent;
border: 1px solid black;
}
.stepper-step-list-item-completed {
background-color: black;
color: white;
}
.stepper-step-list-item span, .stepper-step-list-item svg { margin-left: -3px; }
.stepper-step-list li span { text-align: center;}
.stepper-steps-container {
padding: 10px;
border: 1px solid #D4DDE0;
}
.stepper-step {
}
.stepper-connector {
width: 10%;
height: 10px;
background-color: #D4DDE0;
vertical-align: middle;
display: inline-flex;
margin-top: -18px;
}
.stepper-connector-highlight {
background-color: black;
}
.stepper-button-container{
margin-top: 20px;
background-color: #F2F2F2;
padding: 10px;
}
.stepper-button {
padding: 8px 16px;
margin-left: 5px;
background-color: #F2F2F2;
}
.stepper-primary {
background-color: #3C7AAE;
color: white;
}
@media screen and (max-width: 500px) {
.stepper-step-list{
padding-left: 5px;
}
.stepper-step-list-item {
margin: 0px 5px;
}
}
/* implementation specific */
.field {
padding: 10px;
}
.field input { padding: 6px;}
.hidden { display: none;}
.visible { display: block;}
.field select { padding: 5px;}
Text
{
"errorNodeNotObject": "Node provided is not a DOM object.",
"errorStepsContainer":"Div element with stepper-steps-container class not defined.",
"errorAllowStepChange": "Function allowStepChange not defined using options.",
"errorProcessRequest":"Function processRequest not defined using options.",
"previous": "Previous",
"next": "Next",
"msgEnterFirstName": "Please enter owner's first name",
"msgEnterLastName": "Please enter owner's last name",
"msgEnterOwnerAge": "Please enter owner's age",
"msgOwnerAgeInvalid": "Owner age entered is invalid. Please enter age between 18 to 100.",
"msgEnterCarMake" : "Please enter car's make/company name",
"msgEnterCarModel": "Please enter car's model name",
"msgEnterCarYear": "Please enter the year in which the car was manufactured",
"msgcCarYearInvalid": "Car manufacturing year entered is invalid. Please enter year between 1980 to 2025.",
"msgSelectPolicyType": "Please select policy type",
"msgSelectPolicyClaims": "Please tell us if you have previous claims in the last 5 years",
"msgEnterPolicyDuration": "Please enter the policy duration",
"msgPolicyDurationInvalid": "Policy duration entered is invalid. Please enter the duration between 1 to 5.",
"msgEnterCardNumber": "Please enter the credit/debit card number",
"msgCardNumberInvalid": "Card number entered is invalid. Please enter 16 digit credit/debit card number",
"msgEnterExpDate": "Please enter the expiration date for the card",
"msgPaymentFrequency": "Please select the payment frequency",
"msgEnterCVV": "Please enter the card CVV number",
"msgCVVInvalid": "CVV number entered is invalid. Please enter number between 100 to 999",
"msgDataProcessing": "Data will be submitted for final processing",
"submit": "Submit"
}