Jeremy Threlfall
This small interactive project uses HTML, CSS, and JavaScript to create a step-based progress indicator. It selects all step elements with querySelectorAll and listens for click events on navigation buttons and keyboard arrow presses. When triggered, it updates the active step by toggling CSS classes, and smoothly animates the progress bar’s width using CSS transitions for a polished user experience.
In the HTML, the main container <div> with the class container holds everything—the progress tracker and navigation buttons. Inside it, the .progress-container holds five <div> elements: one with the id and class progress for the blue progress bar, and four .circle elements numbered 1–4 to represent the steps. Finally, two buttons with the class btn and ids prev and next move the progress backward or forward. The first circle starts with the active class, and the “Prev” button is disabled by default so you can’t move backward at the start (this is being applied in JavaScript).
<div class="container">
			<div class="progress-container">
				<div class="progress" id="progress"></div>
				<div class="circle">1</div>
				<div class="circle">2</div>
				<div class="circle">3</div>
				<div class="circle">4</div>
			</div>
			<button class="btn" id="prev">Prev</button>
			<button class="btn" id="next">Next</button>
		</div>On the styling side, the project uses CSS variables (like --active for the blue highlight and --empty for the gray) so color themes can be changed easily from one place. The .container centers everything, and .progress-container uses flexbox to line up the circles. A gray background line (from ::before) sits behind the steps, while the .progress bar—on top—starts at 0% width and grows as you advance. Each .circle is a small white circle with a number; when active, its border turns blue. The .btn elements are styled in blue with white text, rounded corners, and a hover effect that brightens them. Disabled buttons turn gray and lose the hover effect, making it clear they can’t be used.
:root {
	--active: #3498db;
	--empty: #e0e0e0;
	--white: #fff;
	--dark: #222;
	--bg-color: #f6f7fb;
	--font-color: #999;
}
...
.container {
	text-align: center;
}
.progress-container {
	display: flex;
	justify-content: space-between;
	position: relative;
	margin-bottom: 30px;
	max-width: 100%;
	width: 350px;
}
.progress-container::before {
	content: '';
	background-color: var(--empty);
	position: absolute;
	top: 50%;
	left: 0;
	transform: translateY(-50%);
	height: 4px;
	width: 100%;
	z-index: -1;
	transition: 0.4s ease;
}
.progress {
	background-color: var(--active);
	position: absolute;
	top: 50%;
	left: 0;
	transform: translateY(-50%);
	height: 4px;
	width: 0%;
	z-index: -1;
	transition: 0.4s ease;
}
.circle {
	background-color: var(--white);
	color: var(--font-color);
	border-radius: 50%;
	height: 30px;
	width: 30px;
	display: flex;
	justify-content: center;
	align-items: center;
	border: 3px solid var(--empty);
	transition: 0.4s ease;
}
.circle.active {
	border-color: var(--active);
}
.btn {
	background-color: var(--active);
	color: var(--white);
	border: none;
	border-radius: 5px;
	cursor: pointer;
	padding: 8px 30px;
	margin: 5px;
	font-size: 0.9rem;
}
.btn:hover {
	filter: brightness(1.2);
}
.btn:active {
	transform: scale(0.98);
}
.btn:disabled {
	background-color: var(--empty);
	color: var(--dark);
	cursor: not-allowed;
}
.btn:disabled:hover {
	filter: brightness(1);
}The script begins by selecting key elements: the progress bar, buttons, and step circles. A currentActive variable tracks the current step, while maxSteps and splitPercent calculate progress increments. The stepClamp() function ensures the current step stays within range, even if triggered unexpectedly. The updateButtons() function disables the prev or next buttons when the start or end is reached. The updateUI() function applies the active class to all completed steps and adjusts the progress bar width accordingly. Navigation is handled by the moveStep() function, which increments or decrements currentActive based on the direction. Event listeners on the buttons and keydown events (left/right arrow keys) allow for both mouse and keyboard control, improving accessibility.
const progress = document.getElementById('progress');
const prev = document.getElementById('prev');
const next = document.getElementById('next');
const circles = document.querySelectorAll('.circle');
let currentActive = 1;
const maxSteps = circles.length;
const splitPercent = 100 / (circles.length - 1);
// keeps currentActive in range, even if a user programatically gets past the disabled button, and triggers moveStep
const stepClamp = () => {
	currentActive = Math.max(1, Math.min(currentActive, maxSteps));
};
// toggle disabled if the beginning or ending of the step range is reached
const updateButtons = () => {
	prev.disabled = currentActive === 1;
	next.disabled = currentActive === maxSteps;
};
// highlight the current and previously finished steps, adjust width of progress bar
const updateUI = () => {
	circles.forEach((circle, i) => {
		circle.classList.toggle('active', i <= currentActive - 1);
	});
	progress.style.width = `${(currentActive - 1) * splitPercent}%`;
};
const moveStep = (dir) => {
	// in case something triggers moveStep with an unexpected value
	if (dir !== 'prev' && dir !== 'next') return;
	// increment or decrement the currently active step
	dir === 'prev' ? currentActive-- : currentActive++;
	stepClamp();
	updateButtons();
	updateUI();
};
prev.addEventListener('click', () => moveStep('prev'));
next.addEventListener('click', () => moveStep('next'));
// added keydown event listener for accessiblity. clamping comes in handy in this usecase as well
document.addEventListener('keydown', (e) => {
	if (e.key === 'ArrowLeft') moveStep('prev');
	if (e.key === 'ArrowRight') moveStep('next');
});
updateButtons();
updateUI();This approach combines clean HTML structure, reusable CSS with transitions, and concise JavaScript logic to create a polished, responsive, and accessible progress step component.
The result is a clean, interactive progress tracker that not only shows your current step but also responds visually to clicks and key presses.
While this version works well for now, I’m planning to keep it as is and revisit it later with some enhancements. I’m thinking about adding a user interface that lets you input the number of steps dynamically, along with hoverable tooltips that explain what each step represents. Those features would make the component more flexible and user-friendly—a fun project to tackle another day.
Jeremy is a Fullstack Developer. Originally from the United States, he currently resides in Taiwan, and works freelance remotely.
Portfolio