Build a Sorting Visualizer in 100 Lines of JavaScript
The classic sorting-algorithm bar-chart animation looks like it needs a full state machine to pause and resume mid-comparison. It doesn't — JavaScript generator functions let you write the algorithms exactly as you'd find them in a textbook, and get frame-by-frame animation almost for free. This tutorial builds a working visualizer with four algorithms in about 100 lines of plain JavaScript and Canvas 2D.
1. The Core Trick: Generators as Pausable Algorithms
A regular JavaScript function runs to completion the instant you
call it — there's no built-in way to pause a Bubble Sort mid-swap
and resume it 16 milliseconds later on the next animation frame.
A generator function (declared with
function*) solves this exactly: calling
yield suspends execution and hands a value back to
the caller, who can resume it later with .next() —
picking up on the very next line, with all local variables intact.
function* countUp(n) {
for (let i = 0; i < n; i++) yield i; // pauses here every iteration
}
const gen = countUp(3);
gen.next(); // { value: 0, done: false }
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: undefined, done: true }
The whole visualizer is built on one idea: write each sorting
algorithm as a generator that yields every time it
compares or swaps two elements, then let a
requestAnimationFrame loop pull one step at a time and
redraw the array between steps.
2. Data and Canvas Setup
The data is just an array of numbers representing bar heights, and
the renderer draws each one as a coloured vertical bar. Two indices
— compare — get highlighted so you can see exactly
what the algorithm is looking at on each step:
const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);
function drawBars(highlight = []) {
const w = canvas.width / arr.length;
ctx.clearRect(0, 0, canvas.width, canvas.height);
arr.forEach((val, i) => {
ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
const h = (val / 105) * canvas.height;
ctx.fillRect(i * w, canvas.height - h, w - 1, h);
});
}
3. Bubble Sort as a Generator
Bubble Sort repeatedly steps through the array, swapping adjacent
elements that are out of order. Written as a generator, it's
identical to the textbook version except for one added
yield after every comparison:
function* bubbleSort(a) {
for (let i = 0; i < a.length - 1; i++) {
for (let j = 0; j < a.length - i - 1; j++) {
yield [j, j + 1]; // about to compare these two indices
if (a[j] > a[j + 1]) {
[a[j], a[j + 1]] = [a[j + 1], a[j]]; // swap
yield [j, j + 1]; // show the swapped state too
}
}
}
}
4. Insertion Sort as a Generator
Insertion Sort grows a sorted prefix one element at a time, shifting larger elements right to make room. Yielding on every shift visually shows the "insertion" happening in slow motion:
function* insertionSort(a) {
for (let i = 1; i < a.length; i++) {
const key = a[i];
let j = i - 1;
yield [i, j];
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j]; // shift right
j--;
yield [j, j + 1];
}
a[j + 1] = key; // drop the key into its slot
yield [j + 1];
}
}
5. Quick Sort as a Generator
Quick Sort is recursive, so its generator version needs
yield* ("yield delegation") to forward every value
yielded by a recursive call up to the top-level caller — without
it, only the outermost call's yields would ever be seen:
function* quickSort(a, lo = 0, hi = a.length - 1) {
if (lo >= hi) return;
const pivot = a[hi];
let i = lo - 1;
for (let j = lo; j < hi; j++) {
yield [j, hi]; // compare a[j] against the pivot
if (a[j] < pivot) {
i++;
[a[i], a[j]] = [a[j], a[i]];
yield [i, j];
}
}
[a[i + 1], a[hi]] = [a[hi], a[i + 1]]; // place pivot at its final position
yield [i + 1, hi];
yield* quickSort(a, lo, i); // delegate: forward every yield from the left half
yield* quickSort(a, i + 2, hi); // … and the right half
}
yield* and not just calling the
function: a plain recursive call
quickSort(a, lo, i) would run the entire left half to
completion instantly, since nothing is pulling values out of it.
yield* turns the recursive call into a sub-iterator
whose values flow through the parent generator one at a time,
exactly interleaved with the top-level loop pulling frames.
6. Merge Sort as a Generator
Merge Sort splits, recursively sorts both halves, then merges them back using an auxiliary array. Yielding during both the split and the merge-back phase shows the "divide and conquer" structure directly on screen:
function* mergeSort(a, lo = 0, hi = a.length - 1) {
if (lo >= hi) return;
const mid = (lo + hi) >> 1;
yield* mergeSort(a, lo, mid);
yield* mergeSort(a, mid + 1, hi);
const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
let i = 0, j = 0, k = lo;
while (i < left.length && j < right.length) {
a[k] = left[i] <= right[j] ? left[i++] : right[j++];
yield [k]; // index k just received its merged value
k++;
}
while (i < left.length) { a[k] = left[i++]; yield [k++]; }
while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}
7. The Render Loop
The loop pulls one {value, done} pair from the active
generator per animation frame, redraws the bars with the yielded
indices highlighted, and stops when done is true.
Pulling multiple steps per frame controls playback speed without
touching the algorithm code at all:
let sorter = bubbleSort(arr);
let stepsPerFrame = 3; // higher = faster playback
function animate() {
let result;
for (let s = 0; s < stepsPerFrame; s++) {
result = sorter.next();
if (result.done) break;
}
drawBars(result.done ? [] : result.value);
if (!result.done) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
sorter = quickSort(arr) — with
zero changes to the render loop.
8. The Full ~100-Line Visualizer
Putting every piece together into one self-contained script — a working, animated sorting visualizer with four selectable algorithms:
// ── Sorting visualizer in ~100 lines: generators + Canvas 2D ──
const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const N = 80;
let arr = [];
let sorter = null;
const stepsPerFrame = 3;
function resetArray() {
arr = Array.from({ length: N }, () => Math.floor(Math.random() * 100) + 5);
}
function drawBars(highlight = []) {
const w = canvas.width / arr.length;
ctx.clearRect(0, 0, canvas.width, canvas.height);
arr.forEach((val, i) => {
ctx.fillStyle = highlight.includes(i) ? '#f87171' : '#60a5fa';
const h = (val / 105) * canvas.height;
ctx.fillRect(i * w, canvas.height - h, w - 1, h);
});
}
function* bubbleSort(a) {
for (let i = 0; i < a.length - 1; i++)
for (let j = 0; j < a.length - i - 1; j++) {
yield [j, j + 1];
if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; yield [j, j + 1]; }
}
}
function* insertionSort(a) {
for (let i = 1; i < a.length; i++) {
const key = a[i]; let j = i - 1;
yield [i, j];
while (j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; yield [j, j + 1]; }
a[j + 1] = key; yield [j + 1];
}
}
function* quickSort(a, lo = 0, hi = a.length - 1) {
if (lo >= hi) return;
const pivot = a[hi]; let i = lo - 1;
for (let j = lo; j < hi; j++) {
yield [j, hi];
if (a[j] < pivot) { i++; [a[i], a[j]] = [a[j], a[i]]; yield [i, j]; }
}
[a[i + 1], a[hi]] = [a[hi], a[i + 1]]; yield [i + 1, hi];
yield* quickSort(a, lo, i);
yield* quickSort(a, i + 2, hi);
}
function* mergeSort(a, lo = 0, hi = a.length - 1) {
if (lo >= hi) return;
const mid = (lo + hi) >> 1;
yield* mergeSort(a, lo, mid);
yield* mergeSort(a, mid + 1, hi);
const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
let i = 0, j = 0, k = lo;
while (i < left.length && j < right.length) { a[k] = left[i] <= right[j] ? left[i++] : right[j++]; yield [k++]; }
while (i < left.length) { a[k] = left[i++]; yield [k++]; }
while (j < right.length) { a[k] = right[j++]; yield [k++]; }
}
const ALGORITHMS = { bubble: bubbleSort, insertion: insertionSort, quick: quickSort, merge: mergeSort };
function start(name) {
resetArray();
sorter = ALGORITHMS[name](arr);
requestAnimationFrame(animate);
}
function animate() {
let result;
for (let s = 0; s < stepsPerFrame; s++) {
result = sorter.next();
if (result.done) break;
}
drawBars(result.done ? [] : result.value);
if (!result.done) requestAnimationFrame(animate);
}
start('bubble'); // call start('quick'), start('merge'), start('insertion') to switch
<select> dropdown wired to
start(select.value) to switch algorithms live, track a
comparison/swap counter to display Big-O in action empirically, or
add a Web
Audio oscillator whose frequency is driven by the compared
value for the classic "sorting algorithm sound" effect.
Frequently Asked Questions
What will I learn in this tutorial?
Build an animated sorting algorithm visualizer in about 100 lines of plain JavaScript and Canvas 2D: generators as pausable algorithms, Bubble/Insertion/Quick/Merge Sort, and colour-coded comparisons and swaps.
What topics are covered in this tutorial?
This tutorial covers: The Core Trick: Generators as Pausable Algorithms, Data and Canvas Setup, Bubble Sort as a Generator, Insertion Sort as a Generator, Quick Sort as a Generator, Merge Sort as a Generator, The Render Loop, The Full ~100-Line Visualizer.
How long does this tutorial take?
This tutorial takes approximately 25 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.