GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Sorting Algorithm Visualizer: 9 Sort Scenes

Sorting sticks when you watch it move. 04 plants a pivot and sweeps both sides, 07 races four sorts on one array by comparison count, 06 turns scrambled color

Sorting Is Understood Through Motion, Not Values: A Sorting Algorithm Visualizer Starts by Setting Bar Height as the Value and Drawing Each Comparison and Swap One Step at a Time

Auto-plays · click a tile to jump to its section · all nine in one zip

All nine scenes follow one rule. The sort really runs once and writes its comparisons, swaps and writes into a step list, and the screen reads that list on a timetable, one step at a time. Bar heights and the counters both come only from steps that were applied, so what you watch is the computed sort and not a staged animation.

Why the sorting side and the drawing side are split

The sorting side is a pure function such as bubbleSteps, mergeSteps or quickSteps that takes an array and returns a step list, and the drawing side only applies what it receives. To change the algorithm you swap the step generator and leave the bar drawing code alone. Scene 07 stacks this structure four times, so four different step lists play on the same array at once.

Reading values from bar height instead of sound

If you searched for a sorting algorithm visualizer project that plays each value as a tone, this one uses no audio. Values reach you through bar height and a sentence under the stage that says what the current step is.

The layer of calculation each scene adds

01 adds a spring swap with a trail, and 02 a low-frequency spring that lags like inertia. 03 flies bars along a curve between layers, and 04 draws a dotted line at the pivot height. 05 shows a tree and a bar row together, 06 turns slices around a ring, and 07 tallies counts for four lanes. 08 adds gravity and a floor bounce, and 09 brightens a sky by progress.

Sorting Algorithm Visualizer JS: 9 Scenes — Bubble Bars That Swap Neighbors, Insertion Into a Gap, Merge That Splits and Weaves, Quick Sort That Plants a Pivot, Heap Sort Sifting Down a Tree, a Circular Hue Sort, a Race Meter for Four Methods, Radix Pouring Into Buckets and a Self-Sorting Skyline

What nine renders measured for area, frames and intensity

Scene Area covered Frames that moved Mean intensity
01 Bubble bars 29.6% 23/23 66.6
02 Insertion 25.5% 23/23 65.8
03 Merge 10.0% 23/23 84.7
04 Quick 22.6% 22/23 52.8
05 Heap 11.9% 22/23 60.9
06 Hue circle 14.8% 21/23 52.3
07 Race meter 12.7% 23/23 76.4
08 Radix 28.3% 23/23 60.3
09 Skyline 70.8% 23/23 61.2

The whole sky and every building change in 09, so it covers the most at 70.8%, while 03 covers the least at 10.0% because only some of the layered rows change at a time. Mean intensity is highest in 03 at 84.7 and lowest in 06 at 52.3, and 04, 05 and 06 show 22, 22 and 21 moving frames out of 23, so a few frames in those three do not change.

01Bubble Sort Bars That Swap Neighbors and Leave a Trace

A learning site's first screen shows this at a glance, with two bars crossing over each other's slots. Only the swap steps (SWP) from bubbleSteps actually change the row array, and the drawn position sx chases the new slot through the spr spring. A bar crossing to the right lifts slightly through lift so it clears the bar coming the other way, and a faint trail stays behind for TRAIL_S.

Bubble Sort Bars That Swap Neighbors and Leave a Trace
Open in a new tab

01 · Bubble Sort Bars That Swap Neighbors and Leave a Trace — Measured area 29.642% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BAR_COUNTSWAP_TRAVEL_MSbubbleSteps
    function update(dt) {
        trailClock += dt;
        for (let p = 0; p < BAR_COUNT; p += 1) {
            const id = row[p];
            spr(sx[id], slotX(p), dt, SPRING_W, SPRING_Z);
            // a bar crossing to the right lifts slightly so it does not overlap the bar crossing back
            const away = slotX(p) - sx[id].x;
            spr(lift[id], away > 1 ? -Math.min(14, away * 0.35) : 0, dt, 22, 0.8);
            flash[id] = Math.max(0, flash[id] - dt * 2.4);
            const tr = trails[id];
            for (const t of tr) {
                t.age += dt;
            }
            while (tr.length > 0 && tr[0].age > TRAIL_S) {
                tr.shift();
            }
            if (Math.abs(slotX(p) - sx[id].x) > 2.5 && trailClock > 0.012) {
                tr.push({ x: sx[id].x, y: lift[id].x, age: 0 });
            }
        }
        if (trailClock > 0.012) {
            trailClock = 0;
        }
    }

02Insertion Sort That Lifts One Bar and Slots It Into the Gap

Card tidying and priority-list reordering screens use this shape. A LIFT step raises one bar by LIFT_H, each larger bar to its left moves one slot in a SHF step, and DROP lowers the bar into the open gap. The shifting bars use the low-frequency SHIFT_W of 19 and trail their slots a little, while the lifted bar uses 34 and follows the gap faster.

Insertion Sort That Lifts One Bar and Slots It Into the Gap
Open in a new tab

02 · Insertion Sort That Lifts One Bar and Slots It Into the Gap — Measured area 25.461% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LIFT_HSHIFT_WinsertionSteps
    function update(dt) {
        for (let p = 0; p < BAR_COUNT; p += 1) {
            if (row[p] >= 0) {
                spr(sx[row[p]], slotX(p), dt, SHIFT_W, SHIFT_Z);
            }
        }
        for (let id = 0; id < BAR_COUNT; id += 1) {
            if (id === liftId) {
                spr(sx[id], slotX(holeAt), dt, 34, 0.7);
            }
            spr(ly[id], id === liftId ? -LIFT_H : 0, dt, 30, 0.6);
        }
    }

03Merge Sort That Splits in Half and Weaves Two Runs Back Together

This fits a feature that merges two lists by date. Four SPLIT steps open the five layers (LEVELS), and when merging, a CMP step compares the fronts of two runs and a SET step writes the smaller bar into the layer above. A written bar flies along a curve for FLY_S, with the curve's control point 26px above the two slots.

Merge Sort That Splits in Half and Weaves Two Runs Back Together
Open in a new tab

03 · Merge Sort That Splits in Half and Weaves Two Runs Back Together — Measured area 10.017% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLY_SLEVELSmergeSteps
        // flying bar — an upward-bulging curve from a lower-layer slot to an upper-layer slot
        for (const f of flights) {
            const k = ease(f.t);
            const mx = (f.x0 + f.x1) / 2;
            const my = Math.min(f.y0, f.y1) - 26;
            const x = (1 - k) * (1 - k) * f.x0 + 2 * (1 - k) * k * mx + k * k * f.x1;
            const y = (1 - k) * (1 - k) * f.y0 + 2 * (1 - k) * k * my + k * k * f.y1;
            drawBar(x + 2, y, f.v, 1, true);
        }

04Quick Sort That Plants a Pivot Bar and Sweeps Both Sides

Use it to explain how search results are split and sorted quickly. In a PIV step the first bar of the range rises by PIVOT_LIFT to become the pivot, two pointers sweep toward each other swapping any out-of-order pair, and the pivot drops where they meet. A dotted line at the pivot's top height spans the range as the mark the pointers compare against.

Quick Sort That Plants a Pivot Bar and Sweeps Both Sides
Open in a new tab

04 · Quick Sort That Plants a Pivot Bar and Sweeps Both Sides — Measured area 22.576% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

PIVOT_LIFTSWAP_MSquickSteps
        // pivot height dotted line — the pointers compare bar tops against this line
        if (pivId >= 0) {
            const py = BASE_Y - barH(vals[pivId]);
            ctx.setLineDash([5, 4]);
            ctx.lineWidth = 1.5;
            ctx.strokeStyle = rgba(INK, 0.5);
            ctx.beginPath();
            ctx.moveTo(slotX(rangeLo), py);
            ctx.lineTo(slotX(rangeHi + 1), py);
            ctx.stroke();
            ctx.setLineDash([]);
        }

05Heap Sort That Sifts Big Values Down a Binary Tree

This suits an explanation of a scheduler that pops tasks from a priority queue. The same value sits in a tree node and in the bar row with a line tying them together, and three springs per position (tx, ty and bx) chase separately, so the bar moves in the row while its node sinks in the tree. The function treeX divides the width into 2^l equal slots for layer l.

Heap Sort That Sifts Big Values Down a Binary Tree
Open in a new tab

05 · Heap Sort That Sifts Big Values Down a Binary Tree — Measured area 11.944% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

COUNTtreeXheapSteps
    // tree coordinate of position p — layer l splits the width into 2^l equal slots
    function treeX(p) {
        const l = Math.floor(Math.log2(p + 1));
        const k = p + 1 - Math.pow(2, l);
        return (W / Math.pow(2, l)) * (k + 0.5);
    }
    function treeY(p) { return 64 + Math.floor(Math.log2(p + 1)) * 34; }
    function barX(p) { return 21 + p * 29.5 + 12; }

06A Circular Sort That Turns Scrambled Color Slices Into Hue Order

Color palette sorting tools and background decoration use this ring. Hue values become slices around a circle, and selectionSteps scans the remaining range for its minimum and swaps it into place, with that scan alone making 276 comparisons. A swapping slice is pushed inward by popOf by up to RING_POP of 24px, so it looks out of the ring only while it crosses.

A Circular Sort That Turns Scrambled Color Slices Into Hue Order
Open in a new tab

06 · A Circular Sort That Turns Scrambled Color Slices Into Hue Order — Measured area 14.782% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_POPpopOfselectionSteps
    // a slice moves inward in proportion to its angle from its own slot — it looks out of the ring only while it crosses
    function popOf(id, p) {
        return RING_POP * ease(Math.abs(wrapPi(slotAngle(p) - ang[id].x)) / 0.6);
    }
    function update(dt) {
        for (let p = 0; p < COUNT; p += 1) {
            const id = row[p];
            const target = ang[id].x + wrapPi(slotAngle(p) - ang[id].x);
            spr(ang[id], target, dt, SPRING_W, SPRING_Z);
        }
    }

07A Race Meter That Pits Four Sorts on the Same Array by Comparisons and Moves

This is the meter for showing an algorithm choice guide's performance table. The same 14-bar array is copied onto bubble, insertion, merge and quick lanes, each applies one step per STEP_MS at the same time, and each lane raises cnt.c for comparisons and cnt.m for moves. The raising happens in the single laneStep function and only for applied comparison, swap, shift and write steps, while marker steps are skipped with no time.

A Race Meter That Pits Four Sorts on the Same Array by Comparisons and Moves
Open in a new tab

07 · A Race Meter That Pits Four Sorts on the Same Array by Comparisons and Moves — Measured area 12.707% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

STEP_MScountOfapplyLane
    function laneStep(l, s) {
        if (s.k === CMP) {
            cnt[l].c += 1;
            hot[l] = [s.a, s.b];
        }
        else if (s.k === SWP || s.k === SET || s.k === MOV || s.k === SHF) {
            cnt[l].m += 1;
            hot[l] = [s.a, s.b];
        }
        else if (s.k === FIX) {
            sortedFx[l][s.a] = true;
        }
        applyLane(lanes[l], s);
    }

08Radix Sort That Pours Into Digit Buckets and Gathers Back

Sorting scenes with digit-structured values such as postal codes or employee numbers fit this one. For each digit, radixSteps pours balls into buckets with DRP and then takes them out from bucket 0 with TAK, so no two values are compared with each other. A falling ball accelerates at GRAVITY 900px/s² and, if it hits the floor above 70px/s, bounces up at BOUNCE 0.38 of its speed.

Radix Sort That Pours Into Digit Buckets and Gathers Back
Open in a new tab

08 · Radix Sort That Pours Into Digit Buckets and Gathers Back — Measured area 28.308% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYBOUNCEradixSteps
            if (b.mode === 1) {
                b.vy += GRAVITY * dt;
                b.y += b.vy * dt;
                if (b.y >= ry) {
                    b.y = ry;
                    if (b.vy > 70) {
                        b.vy = -b.vy * BOUNCE;
                    }
                    else {
                        b.vy = 0;
                        b.mode = 2;
                    }
                }
            }

09A Hero Where a Jumbled City Skyline Sorts Itself Into a Staircase

This is a landing-page hero for a developer education service. hybridSteps moves buildings with MOV by insertion inside groups of 4 and by merging between groups, and one value sky holds the progress as at divided by the step count so night color, dawn color, stars and sun follow it. One lap ends when the staircase outline is done with the windows lit.

A Hero Where a Jumbled City Skyline Sorts Itself Into a Staircase
Open in a new tab

09 · A Hero Where a Jumbled City Skyline Sorts Itself Into a Staircase — Measured area 70.819% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RUNMOVE_MShybridSteps
        const t = ease(clamp(sky.x, 0, 1));
        skyFill(mix(NIGHT_TOP, DAWN_TOP, t), mix(NIGHT_BOT, DAWN_BOT, t));
        // stars — they fade as the sort progresses
        for (const st of stars) {
            const a = clamp(1 - t * 1.5, 0, 1) * (0.5 + 0.5 * Math.sin(sceneT * 1.4 + st[3] * TAU));
            disc(st[0], st[1], st[2], PAPER, a * 0.85);
        }
        // sun — it rises from below with the progress
        const sy = lerp(318, 112, t);
        disc(388, sy, 62, SUN, 0.18 * t);
        disc(388, sy, 40, SUN, 0.3 * t);
        disc(388, sy, 24, SUN, 0.95 * Math.min(1, t * 1.6));

How Many Bars and How Many Milliseconds per Step Keep a Sorting Algorithm Visualizer Readable?

Using it in place of a chalkboard in class

In a data structures class the useful thing is replaying one algorithm several times, so pause and step buttons matter. For quick sort, a learner should see where the pivot lands one step at a time, which is why 04 uses STEP_MS 75 and 18 bars. When playback is paused and stepped, the counters stop at that step too, so a student can check them against a hand count.

Following along by hand while preparing for interviews

If you are memorizing sorts for an interview, the order that works is to run 07 first, see how comparisons and moves split between the methods, and then copy them by hand. The table below shows what the four lanes of 07 counted on the same 14 bars and what the first lap of each other scene counted.

Algorithm Bars Comparisons Moves
07 Bubble 14 90 52 swaps
07 Insertion 14 62 52 shifts
07 Merge 14 39 54 writes
07 Quick 14 38 14 swaps
01 Bubble 16 119 56 swaps
02 Insertion 14 59 50 shifts
03 Merge 16 44 64 writes
04 Quick 18 71 16 swaps
05 Heap 15 72 47 swaps
06 Selection 24 276 20 swaps
08 Radix 24 0 72 pours and 72 takes
09 Hybrid 16 51 23 moves

On the same 14 bars, comparisons range from 90 for bubble down to 38 for quick, and moves from 14 for quick up to 54 for merge. Rows with different bar counts cannot be compared directly, and 06 uses 276 comparisons on 24 bars to show how many the scanning method spends. Radix does no value comparison, only pouring and taking, so its comparison cell is 0.

Where the line sits for bar count and step gap

For 01 the comparison count is at most n(n−1)/2, which is 120 for 16 bars and 1,128 for 48, and the measured values are 119 and 1,107. A bar is (480−32)/n wide, so 28px at 16 bars and about 9px at 48, and the value label is drawn only when a bar is at least 16px wide. Step gaps are set per step type, so 06 with 276 comparisons scans at SCAN_MS 18 and gives each swap 420ms, keeping the scan fast and the swap visible.

Build the Bar Row, Record Comparisons and Swaps as a Step List, Play One Step at a Time: The Order in Which One Sort Becomes Visible

The screen and the counters come from one place for a single reason. If you count when a step is made and not when it is applied, pausing, stepping and the first scene that starts mid-lap all show the same number.

Counting only when a step is applied

runOne takes one step, lets countStep count the comparison or move, lets applyOne change the screen state, and then raises at by one. A comparison is CMP, a move is SWP, SET, MOV or SHF, and a fixed marker (FIX) or a layer-opening marker is not counted. The isDone and stepFrac helpers are defined in all nine sources, but the drawing loop calls neither, so treat them as checks for use while building.

    function countStep(s) {
        if (s.k === CMP) {
            cmpN += 1;
        }
        else if (s.k === SWP || s.k === SET || s.k === MOV || s.k === SHF) {
            movN += 1;
        }
    }
    function isDone() { return at >= steps.length; }
    // progress of the current step (0 to 1) — 1 once the step ends
    function stepFrac() { return at < steps.length ? clamp(acc / stepDur(steps[at]), 0, 1) : 1; }
    function runOne() {
        const s = steps[at];
        countStep(s);
        applyOne(s);
        at += 1;
    }

A player that applies steps on a timetable

advance applies one step each time the elapsed time acc passes the step length from stepDur. When the lap ends it shows the finished picture for HOLD_S and moves on to the next lap, and the first lap starts with START_FRAC of the steps already applied so the first screen already has a sort in progress. The step length differs by kind, and in 03 a MRG step gets 0.1 seconds.

    function advance(dt) {
        sceneT += dt;
        if (at < steps.length) {
            acc += dt;
            while (at < steps.length && acc >= stepDur(steps[at])) {
                acc -= stepDur(steps[at]);
                runOne();
            }
        }
        else {
            holdT += dt;
            if (holdT >= HOLD_S) {
                lap += 1;
                begin();
            }
        }
        update(dt);
    }

Screen state changes on a swap step

In 01, applyOne changes only the two highlighted slots on a comparison step, swaps two places in row on a swap step, and marks the slot that turns green on a fix step. The order shown by the bars comes only from row, so the screen and the sort result have no way to drift apart.

    function applyOne(s) {
        if (s.k === CMP) {
            hiA = s.a;
            hiB = s.b;
        }
        else if (s.k === SWP) {
            swapAt(row, s.a, s.b);
            flash[row[s.a]] = 1;
            flash[row[s.b]] = 1;
            hiA = s.a;
            hiB = s.b;
        }
        else if (s.k === FIX) {
            fixedPos[s.a] = true;
        }
    }

Where a Sorting Visualizer Breaks: Bars Whose Mid-Swap State Stays on Screen So One Value Looks Doubled, Screens Where the Step Record and Playback Drift Apart So a Finished Sort Shows Mixed Bars and Screens Where Hundreds of Bars Crowd the Steps Together

The first break is a bar that looks like two values in the middle of a swap. In 01, row changes in one swap step but the drawn sx follows through a spring, so two bars cross over each other, and drawing the trail at the same height makes one value read as two. The trail starts at an opacity of 0.34 and fades, and the bar farther from its slot is drawn later so the real bar lands on top of the trail.

The second break is playback drifting from the record. If a counter rises when steps are generated, the total is already on screen in the first frame and disagrees with playback, so this post raises it only inside runOne. Pressing the step button after a lap ends opens the next lap from the start and clears doneSaid again, so the new lap announces its completion once more.

The third break is steps crowding together. When the step gap is shorter than a frame, several steps arrive in one frame, so the first frame after a hidden tab returns is computed only up to DT_MAX of 0.05 seconds and the loop stops and restarts on visibilitychange so the hidden time does not pour out.

The full source and the files for live editing are in the zip below, and the password to extract it is 7qbx64nu. If you were hunting for a sorting algorithm visualizer github repo, this zip holds the whole source in one download.

Change the Bar Count and the Step Gap and How Far Apart Do the Algorithms Grow? Row Length, Playback Speed and Swap Travel Time Compared

Name Changed value Feel
More bars 01 BAR_COUNT 16 → 48 On the same seed, comparisons go from 119 to 1,107 and swaps from 56 to 477, about 9 times each, and a full playback goes from about 16s to about 139s
Shorter step gap 07 STEP_MS 90 → 30 The longest lane (bubble, 142 steps) drops from 12.8s to 4.3s and the comparison and move counts stay the same
Slower swap travel 01 SWAP_TRAVEL_MS 160 → 420 The spring natural frequency falls from 37.5 to about 14.3 rad/s so bars overshoot widely, and a full playback goes from about 16s to about 31s

Things to check before changing a value

At 48 bars each bar thins to about 9px and the value labels vanish, and on the same seed the lap is long enough that watching it to the end takes about 139 seconds. Raising only SWAP_TRAVEL_MS leaves the trail keeping time TRAIL_S as it was, so the tail is cut shorter than the travel path. Shortening STEP_MS in 07 leaves the counts the same, so to see the difference between algorithms you change the bar count and not the gap.

One Sorted Bar Row Under Reduced Motion: Accessibility Through a Sentence Reading the Current Step and Stepping One at a Time

The completion announcement comes once

Each of the nine scenes plays through on its timetable alone, and with focus on the stage, Enter or Space pauses and the right arrow takes one step. The pause button reports its pressed state with aria-pressed, and when the sort ends a completion sentence is written once into a hidden role="status" line.

    let doneSaid = false;
    function checkDone() {
        if (isDone() && !doneSaid) {
            doneSaid = true;
            doneP.textContent = statusText();
        }
    }

The keyboard and a hidden tab

    pad.addEventListener('keydown', function (e) {
        if (e.altKey || e.ctrlKey || e.metaKey) {
            return;
        }
        if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            togglePause();
        }
        else if (e.key === 'ArrowRight') {
            e.preventDefault();
            stepOnce();
        }
    }, { signal: off.signal });

Under prefers-reduced-motion: reduce there is no loop, and showRest applies every step and draws one finished sort with a completion sentence in the status line. During the demo the status line stays at aria-live="off" so it is not read on every step, and it changes to polite once a person first touches the stage.

Sorting Algorithm Visualizer FAQ: Building Without a Library, Checking the Result Is Sorted and Limits When Users Enter Their Own Array

Can I build this sorting algorithm visualizer online without a library

Canvas 2D alone is enough, and none of the nine scenes uses a library, WebGL or a CDN. Random numbers come from the seeded generator seeded, so the same input makes the same step list, and the sort functions know nothing about the DOM, so you can lift one out and run it in Node.

How do I check that the sort came out right

Pass the array after all steps are applied to isSorted, which is in the source. While building this post I ran the eight step generators in Node and all of them returned a sorted result, and the drawing loop does not call this function.

Can I feed in an array the user typed

Apart from 08, where seeded draws distinct values from 100 to 999, the source handled only arrays holding 1 to n once each, and barH turns a value into height by dividing by the bar count, so a duplicate, a negative number or a large value means changing the height and color calculation first, and hundreds of bars bring the long playback seen in the variants table.

For reference, the MDN pages on requestAnimationFrame and Canvas 2D are the baseline, and prefers-reduced-motion covers the reduced-motion setting. For how bars grow on a spring, read the bar chart spring post, and for finding a path across a grid, the maze generator post.

Enter the archive password

The password is inside this article. You will find it as you read.