UI Components Easy
Steps Progress
Multi-step progress indicator with completed, active, and upcoming states — horizontal and vertical orientations.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--text: #f2f6ff;
--muted: #475569;
--border: rgba(255, 255, 255, 0.08);
--accent: #38bdf8;
--accent-dim: rgba(56, 189, 248, 0.15);
--complete: #34d399;
--complete-dim: rgba(52, 211, 153, 0.15);
--transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: Inter, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 520px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: var(--muted);
font-size: 0.875rem;
margin-bottom: 2.5rem;
}
/* ── Stepper ── */
.stepper {
margin-bottom: 0;
}
.steps-track {
display: flex;
align-items: center;
margin-bottom: 0.625rem;
}
/* ── Step ── */
.step {
display: flex;
align-items: center;
flex: 1;
}
.step:last-child {
flex: 0;
}
.step__node {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--border);
background: var(--card);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
transition: border-color var(--transition), background var(--transition), box-shadow
var(--transition);
cursor: default;
z-index: 1;
}
.step__num {
font-size: 0.8125rem;
font-weight: 600;
color: var(--muted);
transition: opacity var(--transition);
}
.step__check {
position: absolute;
inset: 0;
margin: auto;
color: var(--complete);
opacity: 0;
transform: scale(0.6);
transition: opacity var(--transition), transform var(--transition);
}
/* Active */
.step.is-active .step__node {
border-color: var(--accent);
background: var(--accent-dim);
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.12);
}
.step.is-active .step__num {
color: var(--accent);
}
/* Complete */
.step.is-complete .step__node {
border-color: var(--complete);
background: var(--complete-dim);
}
.step.is-complete .step__num {
opacity: 0;
}
.step.is-complete .step__check {
opacity: 1;
transform: scale(1);
}
/* ── Connector ── */
.step__connector {
flex: 1;
height: 2px;
background: var(--border);
position: relative;
overflow: hidden;
margin: 0 4px;
border-radius: 1px;
}
.step__fill {
display: block;
height: 100%;
width: 0%;
background: var(--complete);
border-radius: 1px;
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.step.is-complete .step__fill {
width: 100%;
}
/* ── Step labels ── */
.steps-labels {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
}
.step-label {
font-size: 0.72rem;
font-weight: 500;
color: var(--muted);
text-align: center;
transition: color var(--transition);
width: 36px;
text-align: center;
}
.step-label--active {
color: var(--accent);
font-weight: 600;
}
.step-label--complete {
color: var(--complete);
}
/* ── Step panel ── */
.step-panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.5rem;
margin-bottom: 1.5rem;
min-height: 100px;
transition: opacity 0.2s, transform 0.2s;
}
.step-panel.fade {
opacity: 0;
transform: translateX(8px);
}
.panel-title {
font-size: 1.0625rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.panel-desc {
font-size: 0.875rem;
color: var(--muted);
line-height: 1.6;
}
/* ── Actions ── */
.step-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.btn {
padding: 0.5rem 1.25rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: opacity 0.2s, background 0.2s, color 0.2s;
}
.btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.btn--ghost {
background: rgba(255, 255, 255, 0.06);
color: var(--text);
border: 1px solid var(--border);
}
.btn--ghost:not(:disabled):hover {
background: rgba(255, 255, 255, 0.1);
}
.btn--primary {
background: var(--accent);
color: #050910;
}
.btn--primary:not(:disabled):hover {
opacity: 0.85;
}
.btn--success {
background: var(--complete);
color: #050910;
}(function () {
var STEPS = [
{
title: "Create your account",
desc: "Enter your email and choose a secure password to get started.",
},
{ title: "Personal details", desc: "Tell us your name, location, and a bit about yourself." },
{
title: "Payment information",
desc: "Add a payment method — you won't be charged until you confirm.",
},
{
title: "Review and confirm",
desc: "Double-check your details. Everything looks good? Let's go!",
},
];
var current = 0;
var stepEls = document.querySelectorAll(".step");
var labels = document.querySelectorAll(".step-label");
var panel = document.getElementById("step-panel");
var title = document.getElementById("panel-title");
var desc = document.getElementById("panel-desc");
var btnPrev = document.getElementById("btn-prev");
var btnNext = document.getElementById("btn-next");
function render() {
stepEls.forEach(function (el, i) {
el.classList.toggle("is-active", i === current);
el.classList.toggle("is-complete", i < current);
var node = el.querySelector(".step__node");
if (node) {
if (i === current) {
node.setAttribute("aria-current", "step");
} else {
node.removeAttribute("aria-current");
}
}
});
labels.forEach(function (lbl, i) {
lbl.classList.toggle("step-label--active", i === current);
lbl.classList.toggle("step-label--complete", i < current);
});
// Animate panel transition
if (panel) {
panel.classList.add("fade");
setTimeout(function () {
if (title) title.textContent = STEPS[current].title;
if (desc) desc.textContent = STEPS[current].desc;
panel.classList.remove("fade");
}, 160);
}
if (btnPrev) btnPrev.disabled = current === 0;
if (btnNext) {
if (current === STEPS.length - 1) {
btnNext.textContent = "Finish";
btnNext.className = "btn btn--success";
} else {
btnNext.textContent = "Next";
btnNext.className = "btn btn--primary";
}
}
}
if (btnPrev) {
btnPrev.addEventListener("click", function () {
if (current > 0) {
current--;
render();
}
});
}
if (btnNext) {
btnNext.addEventListener("click", function () {
if (current < STEPS.length - 1) {
current++;
render();
} else {
// Reset demo on Finish
current = 0;
render();
}
});
}
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Steps Progress</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Steps Progress</h1>
<p class="demo-sub">Multi-step progress indicator with completed, active, and upcoming states.</p>
<!-- Stepper -->
<div class="stepper" id="stepper" aria-label="Form progress">
<div class="steps-track">
<div class="step is-active" data-step="0">
<div class="step__node" aria-current="step" aria-label="Step 1: Account — current">
<span class="step__num" aria-hidden="true">1</span>
<svg class="step__check" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline points="2.5 7 5.5 10 11.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="step__connector" aria-hidden="true">
<span class="step__fill"></span>
</div>
</div>
<div class="step" data-step="1">
<div class="step__node" aria-label="Step 2: Details — upcoming">
<span class="step__num" aria-hidden="true">2</span>
<svg class="step__check" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline points="2.5 7 5.5 10 11.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="step__connector" aria-hidden="true">
<span class="step__fill"></span>
</div>
</div>
<div class="step" data-step="2">
<div class="step__node" aria-label="Step 3: Payment — upcoming">
<span class="step__num" aria-hidden="true">3</span>
<svg class="step__check" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline points="2.5 7 5.5 10 11.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="step__connector" aria-hidden="true">
<span class="step__fill"></span>
</div>
</div>
<div class="step" data-step="3">
<div class="step__node" aria-label="Step 4: Confirm — upcoming">
<span class="step__num" aria-hidden="true">4</span>
<svg class="step__check" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="none">
<polyline points="2.5 7 5.5 10 11.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<!-- No connector on last step -->
</div>
</div>
<!-- Labels row -->
<div class="steps-labels" aria-hidden="true">
<span class="step-label step-label--active">Account</span>
<span class="step-label">Details</span>
<span class="step-label">Payment</span>
<span class="step-label">Confirm</span>
</div>
</div>
<!-- Step content -->
<div class="step-panel" id="step-panel">
<p class="panel-title" id="panel-title">Create your account</p>
<p class="panel-desc" id="panel-desc">Enter your email and choose a secure password to get started.</p>
</div>
<!-- Navigation -->
<div class="step-actions">
<button class="btn btn--ghost" id="btn-prev" disabled>Previous</button>
<button class="btn btn--primary" id="btn-next">Next</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Steps Progress
A multi-step progress indicator that guides users through a sequential workflow. Supports completed, active, and upcoming states with smooth CSS transitions.
Features
- Four-step horizontal stepper with connecting progress line
- Completed steps show a checkmark icon
- Active step is highlighted with accent color and a pulsing ring
- Upcoming steps are muted
- Previous / Next buttons navigate between steps
- Progress line fills proportionally as steps are completed
How it works
- Each step circle transitions through three CSS classes: default (upcoming),
.is-active, and.is-complete - The connecting line between steps uses a
::afterpseudo-element that fills with a CSS transition based on a--progresscustom property - JS increments/decrements the current step index and applies the appropriate classes
Accessibility
Uses aria-current="step" on the active step and aria-label with status on each step node for screen reader context.