Loading
Create a web app that animates sorting algorithms with adjustable speed controls and visual comparisons.
You'll build a web application that visualizes sorting algorithms in real time. Users can watch bubble sort, merge sort, and quicksort operate on randomized arrays, control animation speed, and compare algorithm performance side by side. The entire project uses vanilla JavaScript and DOM manipulation — no frameworks, no dependencies.
What you'll learn:
requestAnimationFrameCreate the following files:
Start with the HTML shell:
The visualizer renders bars whose heights represent array values. Use flexbox to lay them out horizontally:
This class manages the array state and renders bars to the DOM. It exposes methods the sorting algorithms will call to animate swaps and comparisons.
Bubble sort is the simplest — compare adjacent pairs and swap if out of order. Each pass bubbles the largest unsorted element to its final position.
Merge sort divides the array recursively, then merges sorted halves. The visualization writes values back into the original array at the correct indices.
Quicksort picks a pivot, partitions elements around it, then recurses on both sides. The pivot selection uses the last element for simplicity.
Connect the UI controls to the visualizer and algorithm modules:
The speed slider maps to the delay between operations. At maximum speed (100), the delay is 1ms. At minimum (1), it's 100ms. The sleep() method in the Visualizer reads this.delay on every call, so speed changes take effect immediately mid-sort.
Add a "Stop" button for interrupting long sorts:
Enhance the visualization with gradient colors based on bar height. This makes it easier to see when elements are in their correct position:
Add an audio cue for comparisons using the Web Audio API:
Add a side-by-side mode that runs two algorithms simultaneously on identical arrays:
Add this CSS for the split view:
To serve the project, run npx serve . in the project directory and open it in your browser. Experiment with different array sizes and speeds to observe how O(n²) bubble sort slows dramatically compared to O(n log n) merge and quicksort as the array grows.
algorithm-visualizer/
├── index.html
├── style.css
├── main.js
├── algorithms/
│ ├── bubble.js
│ ├── merge.js
│ └── quick.js
└── visualizer.js* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #0a0a0f;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
padding: 1rem 2rem;
display: flex;
align-items: center;
gap: 2rem;
flex-wrap: wrap;
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
#visualizer {
flex: 1;
display: flex;
align-items: flex-end;
gap: 1px;
padding: 2rem;
}
.bar {
flex: 1;
background: var(--bar-color, #10b981);
transition: background 0.05s;
border-radius: 2px 2px 0 0;
}
.bar.comparing {
background: #f59e0b;
}
.bar.swapping {
background: #ef4444;
}
.bar.sorted {
background: #06b6d4;
}
button,
select {
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: inherit;
cursor: pointer;
}
button:hover {
background: rgba(255, 255, 255, 0.1);
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}.viz-half {
flex: 1;
display: flex;
align-items: flex-end;
gap: 1px;
height: 100%;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.viz-half:last-child {
border-right: none;
}// visualizer.js
export class Visualizer {
constructor(container, statsEl) {
this.container = container;
this.statsEl = statsEl;
this.array = [];
this.comparisons = 0;
this.swaps = 0;
this.delay = 20;
this.running = false;
}
generate(size) {
this.array = Array.from({ length: size }, () => Math.floor(Math.random() * 400) + 10);
this.comparisons = 0;
this.swaps = 0;
this.render();
}
render() {
this.container.innerHTML = "";
const max = Math.max(...this.array);
this.array.forEach((val) => {
const bar = document.createElement("div");
bar.className = "bar";
bar.style.height = `${(val / max) * 100}%`;
this.container.appendChild(bar);
});
this.updateStats();
}
async highlight(indices, className) {
const bars = this.container.children;
indices.forEach((i) => bars[i]?.classList.add(className));
await this.sleep();
indices.forEach((i) => bars[i]?.classList.remove(className));
}
async compare(i, j) {
this.comparisons++;
await this.highlight([i, j], "comparing");
this.updateStats();
return this.array[i] - this.array[j];
}
async swap(i, j) {
this.swaps++;
[this.array[i], this.array[j]] = [this.array[j], this.array[i]];
await this.highlight([i, j], "swapping");
this.renderBars();
this.updateStats();
}
renderBars() {
const bars = this.container.children;
const max = Math.max(...this.array);
this.array.forEach((val, i) => {
if (bars[i]) bars[i].style.height = `${(val / max) * 100}%`;
});
}
markSorted(index) {
this.container.children[index]?.classList.add("sorted");
}
sleep() {
return new Promise((r) => setTimeout(r, this.delay));
}
updateStats() {
this.statsEl.textContent = `Comparisons: ${this.comparisons} | Swaps: ${this.swaps}`;
}
}// algorithms/bubble.js
export async function bubbleSort(viz) {
const n = viz.array.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (!viz.running) return;
const cmp = await viz.compare(j, j + 1);
if (cmp > 0) {
await viz.swap(j, j + 1);
}
}
viz.markSorted(n - i - 1);
}
viz.markSorted(0);
}// algorithms/merge.js
export async function mergeSort(viz) {
await mergeSortHelper(viz, 0, viz.array.length - 1);
for (let i = 0; i < viz.array.length; i++) viz.markSorted(i);
}
async function mergeSortHelper(viz, left, right) {
if (left >= right || !viz.running) return;
const mid = Math.floor((left + right) / 2);
await mergeSortHelper(viz, left, mid);
await mergeSortHelper(viz, mid + 1, right);
await merge(viz, left, mid, right);
}
async function merge(viz, left, mid, right) {
const leftArr = viz.array.slice(left, mid + 1);
const rightArr = viz.array.slice(mid + 1, right + 1);
let i = 0,
j = 0,
k = left;
while (i < leftArr.length && j < rightArr.length && viz.running) {
viz.comparisons++;
await viz.highlight([k], "comparing");
if (leftArr[i] <= rightArr[j]) {
viz.array[k] = leftArr[i++];
} else {
viz.array[k] = rightArr[j++];
}
viz.renderBars();
viz.updateStats();
k++;
}
while (i < leftArr.length) {
viz.array[k++] = leftArr[i++];
}
while (j < rightArr.length) {
viz.array[k++] = rightArr[j++];
}
viz.renderBars();
}// algorithms/quick.js
export async function quickSort(viz) {
await quickSortHelper(viz, 0, viz.array.length - 1);
for (let i = 0; i < viz.array.length; i++) viz.markSorted(i);
}
async function quickSortHelper(viz, low, high) {
if (low >= high || !viz.running) return;
const pivotIndex = await partition(viz, low, high);
viz.markSorted(pivotIndex);
await quickSortHelper(viz, low, pivotIndex - 1);
await quickSortHelper(viz, pivotIndex + 1, high);
}
async function partition(viz, low, high) {
const pivot = viz.array[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (!viz.running) return low;
viz.comparisons++;
await viz.highlight([j, high], "comparing");
viz.updateStats();
if (viz.array[j] < pivot) {
i++;
await viz.swap(i, j);
}
}
await viz.swap(i + 1, high);
return i + 1;
}// main.js
import { Visualizer } from "./visualizer.js";
import { bubbleSort } from "./algorithms/bubble.js";
import { mergeSort } from "./algorithms/merge.js";
import { quickSort } from "./algorithms/quick.js";
const algorithms = { bubble: bubbleSort, merge: mergeSort, quick: quickSort };
const viz = new Visualizer(document.getElementById("visualizer"), document.getElementById("stats"));
const sizeInput = document.getElementById("size");
const speedInput = document.getElementById("speed");
const algoSelect = document.getElementById("algorithm");
const generateBtn = document.getElementById("generate");
const startBtn = document.getElementById("start");
function generateArray() {
viz.running = false;
viz.generate(parseInt(sizeInput.value));
startBtn.disabled = false;
}
speedInput.addEventListener("input", () => {
viz.delay = Math.max(1, 101 - parseInt(speedInput.value));
});
generateBtn.addEventListener("click", generateArray);
startBtn.addEventListener("click", async () => {
startBtn.disabled = true;
generateBtn.disabled = true;
viz.running = true;
const algo = algorithms[algoSelect.value];
await algo(viz);
viz.running = false;
generateBtn.disabled = false;
});
generateArray();// Add to controls in index.html:
// <button id="stop" disabled>Stop</button>
const stopBtn = document.getElementById("stop");
startBtn.addEventListener("click", async () => {
startBtn.disabled = true;
stopBtn.disabled = false;
generateBtn.disabled = true;
viz.running = true;
const algo = algorithms[algoSelect.value];
const startTime = performance.now();
await algo(viz);
const elapsed = performance.now() - startTime;
viz.running = false;
stopBtn.disabled = true;
generateBtn.disabled = false;
viz.statsEl.textContent += ` | Time: ${(elapsed / 1000).toFixed(2)}s`;
});
stopBtn.addEventListener("click", () => {
viz.running = false;
stopBtn.disabled = true;
generateBtn.disabled = false;
});// Add to Visualizer.render()
render() {
this.container.innerHTML = "";
const max = Math.max(...this.array);
this.array.forEach((val) => {
const bar = document.createElement("div");
bar.className = "bar";
const pct = val / max;
bar.style.height = `${pct * 100}%`;
// Hue shifts from 160 (teal) to 280 (purple) based on value
bar.style.setProperty("--bar-color", `hsl(${160 + pct * 120}, 70%, 55%)`);
bar.style.background = "var(--bar-color)";
this.container.appendChild(bar);
});
this.updateStats();
}const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playTone(frequency, duration = 0.05) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.frequency.value = frequency;
gain.gain.value = 0.05;
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
// Call in compare(): playTone(200 + (array[i] / max) * 800);async function runComparison() {
const size = parseInt(sizeInput.value);
const baseArray = Array.from({ length: size }, () => Math.floor(Math.random() * 400) + 10);
// Create two visualizer containers
const main = document.getElementById("visualizer");
main.innerHTML = "";
const left = document.createElement("div");
const right = document.createElement("div");
left.className = "viz-half";
right.className = "viz-half";
main.appendChild(left);
main.appendChild(right);
const vizA = new Visualizer(left, document.getElementById("stats"));
const vizB = new Visualizer(right, document.getElementById("stats"));
vizA.array = [...baseArray];
vizB.array = [...baseArray];
vizA.render();
vizB.render();
vizA.running = true;
vizB.running = true;
vizA.delay = viz.delay;
vizB.delay = viz.delay;
// Run both simultaneously
await Promise.all([bubbleSort(vizA), quickSort(vizB)]);
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Algorithm Visualizer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<h1>Algorithm Visualizer</h1>
<div class="controls">
<select id="algorithm">
<option value="bubble">Bubble Sort</option>
<option value="merge">Merge Sort</option>
<option value="quick">Quick Sort</option>
</select>
<label> Size: <input type="range" id="size" min="10" max="200" value="50" /> </label>
<label> Speed: <input type="range" id="speed" min="1" max="100" value="50" /> </label>
<button id="generate">New Array</button>
<button id="start">Sort</button>
</div>
</header>
<main id="visualizer"></main>
<footer id="stats"></footer>
<script type="module" src="main.js"></script>
</body>
</html>