~/blog/making_of_sorting_visualizer
cd ..
Ξ~/blog/making_of_sorting_visualizercat making_of_sorting_visualizer.md

Building a Sorting Algorithm Visualizer

Published:2025-10-20
Read time:18 min read

How I Built a Sorting Algorithm Visualizer in Vanilla JavaScript (So You Don't Have to Use jQuery in 2025)

Posted on December 21, 2025 | Reading time: ~15 minutes (or 30 if you keep getting distracted by the visualizations)


Introduction: Why I Did This to Myself

So there I was, sitting at my desk at 2 AM, convinced that what the world really needed was a way to watch sorting algorithms in action. Not for any practical reason—no, just for the pure aesthetic joy of watching bars shuffle around on your screen while lo-fi beats play in the background.

The result? A fully functional sorting algorithm visualizer built with pure vanilla JavaScript, HTML, and CSS. No frameworks. No build tools. Just raw JavaScript doing the heavy lifting, like we're living in 2015 (but with modern async/await because I'm not a monster).

In this post, I'll walk you through exactly how I built it, from the HTML skeleton to the actual sorting algorithms. Let's dive in.


Part 1: The HTML Structure (aka "The Container for Our Chaos")

First things first, we need a place to put all our visual bars. Here's the HTML foundation:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sorting Visualizer - Vanilla JS</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <!-- Navigation Bar -->
    <nav class="navbar">
        <div class="nav-container">
            <h1 class="nav-title">Sorting Visualizer</h1>
            
            <div class="nav-controls">
                <!-- Algorithm Selection -->
                <div class="nav-group">
                    <button id="generate-array-btn" class="btn btn-secondary">Generate New Array</button>
                    <div class="divider"></div>
                    <button class="btn btn-algo" data-algo="merge">Merge Sort</button>
                    <button class="btn btn-algo" data-algo="quick">Quick Sort</button>
                    <button class="btn btn-algo" data-algo="heap">Heap Sort</button>
                    <button class="btn btn-algo" data-algo="bubble">Bubble Sort</button>
                </div>

                <!-- Simulation Controls (Right aligned) -->
                <div class="nav-group right">
                    <!-- Size Control -->
                    <div class="control-group">
                        <label for="size-slider">Size</label>
                        <input type="range" id="size-slider" min="5" max="100" value="50">
                    </div>

                    <!-- Speed Control -->
                    <div class="control-group">
                        <label for="speed-slider">Speed</label>
                        <input type="range" id="speed-slider" min="1" max="100" value="50">
                    </div>

                    <button id="pause-btn" class="btn btn-warning" disabled>Pause</button>
                    <button id="reset-btn" class="btn btn-danger" disabled>Reset</button>
                </div>
            </div>
        </div>
    </nav>

    <!-- Legend -->
    <div class="legend">
        <div class="legend-item">
            <div class="legend-box default-bar"></div>
            <span>Unsorted</span>
        </div>
        <div class="legend-item">
            <div class="legend-box compare-bar"></div>
            <span>Compare</span>
        </div>
        <div class="legend-item">
            <div class="legend-box swap-bar"></div>
            <span>Swap</span>
        </div>
        <div class="legend-item">
            <div class="legend-box sorted-bar"></div>
            <span>Sorted</span>
        </div>
    </div>

    <!-- Main Container -->
    <div id="visualizer-container">
        <!-- Bars will be generated here by JavaScript -->
    </div>

    <!-- Scripts -->
    <script src="js/helper.js"></script>
    <script src="js/sorting-algorithms.js"></script>
    <script src="js/visualizer.js"></script>
</body>
</html>

Key things here:

  • Buttons with data-algo attributes so we can identify which algorithm to run
  • Sliders for controlling size and speed (because sometimes you want to watch bars slowly arrange themselves)
  • A container that will hold all our beautiful bars
  • Script order matters! Load helpers first, then algorithms, then the main visualizer logic

  • Part 2: The CSS (aka "Making It Not Look Like It's From 1998")

Now for the styling. This is where we make things pretty while ensuring the bars stay aligned at the bottom like a proper bar chart:

/* Reset and Base Styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    user-select: none; /* Prevent text selection during rapid clicking */
}

body {
    font-family: 'Arial', sans-serif;
    background-color: #f5f5f5;
    color: #333;
    line-height: 1.6;
    height: 100vh;
    display: flex;
    flex-direction: column;
}

/* Navigation Bar */
.navbar {
    background-color: #34495e;
    color: white;
    padding: 1rem 0;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Buttons */
.btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 500;
    transition: all 0.2s ease;
    background-color: transparent;
    color: #ecf0f1;
}

.btn:hover:not(:disabled) {
    background-color: rgba(255, 255, 255, 0.1);
}

.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.btn-algo.active {
    color: #3498db;
    font-weight: bold;
}

/* Sliders */
.control-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0 0.5rem;
}

.control-group label {
    font-size: 0.75rem;
    color: #bdc3c7;
    margin-bottom: 2px;
}

input[type="range"] {
    width: 100px;
    cursor: pointer;
}

/* Legend */
.legend {
    display: flex;
    justify-content: center;
    padding: 1rem;
    background-color: white;
    margin-bottom: 2px;
}

.legend-item {
    display: flex;
    align-items: center;
    margin: 0 1rem;
    font-size: 0.9rem;
}

.legend-box {
    width: 20px;
    height: 20px;
    margin-right: 5px;
    border-radius: 2px;
}

.default-bar { background-color: #40E0D0; /* Turquoise */ }
.compare-bar { background-color: #e74c3c; /* Red */ }
.swap-bar { background-color: #f1c40f; /* Yellow */ }
.sorted-bar { background-color: #2ecc71; /* Green */ }

/* Visualizer Container */
#visualizer-container {
    flex: 1;
    background-color: #ecf0f1;
    display: flex;
    align-items: flex-end; /* Align bars to bottom */
    justify-content: center;
    padding: 20px;
    overflow: hidden;
    position: relative;
}

.array-bar {
    background-color: #40E0D0; /* Default: Turquoise */
    margin: 0 1px;
    display: inline-block;
    transition: background-color 0.1s ease;
}

The magic here:

  • user-select: none prevents that annoying text selection when you're spamming buttons
  • flex-end on the container aligns bars to the bottom (like a proper chart)
  • Color transitions are quick (0.1s) so the bars feel responsive
  • The legend shows what each color means (because aesthetics + education = profit)

  • Part 3: The Helper Functions (aka "Utility Belt 101")

Before we get to the fancy algorithms, let's establish some utility functions that every program needs:

// Helper Methods

/**
 * Returns a random integer between min (inclusive) and max (exclusive).
 */
function randomIntFromInterval(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

/**
 * Sleeps for a specified amount of milliseconds.
 * Useful when you need to pause async operations without blocking the UI.
 */
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Swaps two elements in an array.
 * Note: This modifies the array in place.
 */
function swap(array, i, j) {
    const temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

Why these matter:

  • randomIntFromInterval() generates our initial array values
  • sleep() lets us pause execution asynchronously without blocking the UI (this is crucial!)
  • swap() is used by every sorting algorithm, so why repeat yourself?

  • Part 4: The Sorting Algorithms (aka "The Heart of the Matter")

This is where the real magic happens. Each algorithm returns an array of "animations" that tell the visualizer what to do.

The Animation Protocol: Each animation is a simple array: [type, val1, val2, val3, val4]

  • "compare": Highlight two bars being compared (red)
  • "revert": Return bars to normal color
  • "swap": Physically change the heights of two bars
  • "overwrite": Change a single bar's height (for merge sort)
  • Bubble Sort

    function getBubbleSortAnimations(array) {
        const animations = [];
        if (array.length <= 1) return animations;
    
        const n = array.length;
        for (let i = 0; i < n - 1; i++) {
            for (let j = 0; j < n - i - 1; j++) {
                // 1. Compare j and j+1
                animations.push(["compare", j, j + 1]);
                // 2. Revert color
                animations.push(["revert", j, j + 1]);
    
                if (array[j] > array[j + 1]) {
                    // 3. Swap
                    animations.push(["swap", j, array[j + 1], j + 1, array[j]]);
                    swap(array, j, j + 1);
                }
            }
        }
        return animations;
    }

Simple, inefficient, but oh-so-satisfying to watch. It's like watching someone organize their desk very slowly.

Merge Sort

function getMergeSortAnimations(array) {
    const animations = [];
    if (array.length <= 1) return animations;
    const auxiliaryArray = array.slice();
    mergeSortHelper(array, 0, array.length - 1, auxiliaryArray, animations);
    return animations;
}

function mergeSortHelper(mainArray, startIdx, endIdx, auxiliaryArray, animations) {
    if (startIdx === endIdx) return;
    const middleIdx = Math.floor((startIdx + endIdx) / 2);

    mergeSortHelper(auxiliaryArray, startIdx, middleIdx, mainArray, animations);
    mergeSortHelper(auxiliaryArray, middleIdx + 1, endIdx, mainArray, animations);

    doMerge(mainArray, startIdx, middleIdx, endIdx, auxiliaryArray, animations);
}

function doMerge(mainArray, startIdx, middleIdx, endIdx, auxiliaryArray, animations) {
    let k = startIdx;
    let i = startIdx;
    let j = middleIdx + 1;

    while (i <= middleIdx && j <= endIdx) {
        // Compare values at i and j
        animations.push(["compare", i, j]);
        animations.push(["revert", i, j]);

        if (auxiliaryArray[i] <= auxiliaryArray[j]) {
            animations.push(["overwrite", k, auxiliaryArray[i]]);
            mainArray[k++] = auxiliaryArray[i++];
        } else {
            animations.push(["overwrite", k, auxiliaryArray[j]]);
            mainArray[k++] = auxiliaryArray[j++];
        }
    }

    // Handle remaining elements
    while (i <= middleIdx) {
        animations.push(["compare", i, i]);
        animations.push(["revert", i, i]);
        animations.push(["overwrite", k, auxiliaryArray[i]]);
        mainArray[k++] = auxiliaryArray[i++];
    }

    while (j <= endIdx) {
        animations.push(["compare", j, j]);
        animations.push(["revert", j, j]);
        animations.push(["overwrite", k, auxiliaryArray[j]]);
        mainArray[k++] = auxiliaryArray[j++];
    }
}

Merge Sort is the overachiever of algorithms—it's efficient and looks damn good when animated. It's like watching someone methodically organize their life (we all aspire to this).

Quick Sort

function getQuickSortAnimations(array) {
    const animations = [];
    if (array.length <= 1) return animations;
    quickSortHelper(array, 0, array.length - 1, animations);
    return animations;
}

function quickSortHelper(array, startIdx, endIdx, animations) {
    if (startIdx >= endIdx) return;

    const pivotIdx = partition(array, startIdx, endIdx, animations);
    quickSortHelper(array, startIdx, pivotIdx - 1, animations);
    quickSortHelper(array, pivotIdx + 1, endIdx, animations);
}

function partition(array, startIdx, endIdx, animations) {
    const pivotValue = array[endIdx];
    let pivotIdx = startIdx;

    for (let i = startIdx; i < endIdx; i++) {
        animations.push(["compare", i, endIdx]);
        animations.push(["revert", i, endIdx]);

        if (array[i] < pivotValue) {
            animations.push(["swap", i, array[pivotIdx], pivotIdx, array[i]]);
            swap(array, i, pivotIdx);
            pivotIdx++;
        }
    }

    animations.push(["swap", pivotIdx, array[endIdx], endIdx, array[pivotIdx]]);
    swap(array, pivotIdx, endIdx);
    return pivotIdx;
}

Quick Sort is the chaos agent. Sometimes it's fast, sometimes it's not. Just like my morning routine.

Heap Sort

function getHeapSortAnimations(array) {
    const animations = [];
    if (array.length <= 1) return animations;

    const n = array.length;

    // Build heap
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(array, n, i, animations);
    }

    // Extract elements one by one
    for (let i = n - 1; i > 0; i--) {
        animations.push(["swap", 0, array[i], i, array[0]]);
        swap(array, 0, i);
        heapify(array, i, 0, animations);
    }
    return animations;
}

function heapify(array, n, i, animations) {
    let largest = i;
    let left = 2 * i + 1;
    let right = 2 * i + 2;

    if (left < n) {
        animations.push(["compare", left, largest]);
        animations.push(["revert", left, largest]);
        if (array[left] > array[largest]) {
            largest = left;
        }
    }

    if (right < n) {
        animations.push(["compare", right, largest]);
        animations.push(["revert", right, largest]);
        if (array[right] > array[largest]) {
            largest = right;
        }
    }

    if (largest !== i) {
        animations.push(["swap", i, array[largest], largest, array[i]]);
        swap(array, i, largest);
        heapify(array, n, largest, animations);
    }
}

Heap Sort? More like "Heap of Complexity." But it's a solid O(n log n) algorithm that never disappoints.


Part 5: The Main Visualizer (aka "Where It All Comes Together")

Now for the orchestrator—the JavaScript class that ties everything together:

const PRIMARY_COLOR = '#40E0D0'; // Turquoise
const SECONDARY_COLOR = '#e74c3c'; // Red
const SORTED_COLOR = '#2ecc71'; // Green
const SWAP_COLOR = '#f1c40f'; // Yellow

class Visualizer {
    constructor() {
        // DOM Elements
        this.container = document.getElementById('visualizer-container');
        this.array = [];
        this.bars = [];
        this.sizeSlider = document.getElementById('size-slider');
        this.speedSlider = document.getElementById('speed-slider');
        this.genBtn = document.getElementById('generate-array-btn');
        this.pauseBtn = document.getElementById('pause-btn');
        this.resetBtn = document.getElementById('reset-btn');
        this.algoBtns = document.querySelectorAll('.btn-algo');

        // State
        this.size = parseInt(this.sizeSlider.value);
        this.speed = parseInt(this.speedSlider.value);
        this.delay = this.calculateDelay(this.speed);
        this.isSorting = false;
        this.isPaused = false;
        this.currentAlgo = null;
        this.abortController = null;

        this.init();
    }

    init() {
        this.resetArray();
        this.addEventListeners();
    }

    addEventListeners() {
        this.genBtn.addEventListener('click', () => {
            if (this.isSorting) return;
            this.resetArray();
        });

        this.sizeSlider.addEventListener('input', (e) => {
            if (this.isSorting && !this.isPaused) return;
            this.size = parseInt(e.target.value);
            this.resetArray();
        });

        this.speedSlider.addEventListener('input', (e) => {
            this.speed = parseInt(e.target.value);
            this.delay = this.calculateDelay(this.speed);
        });

        this.algoBtns.forEach(btn => {
            btn.addEventListener('click', () => {
                if (this.isSorting) return;
                this.selectAlgo(btn);
            });
        });

        this.pauseBtn.addEventListener('click', () => {
            if (!this.isSorting) return;
            if (this.isPaused) {
                this.resume();
            } else {
                this.pause();
            }
        });

        this.resetBtn.addEventListener('click', () => {
            this.reset();
        });
    }

    calculateDelay(speedVal) {
        // Speed slider: 1 (slow) to 100 (fast)
        // Delay: High (e.g. 500ms) to Low (e.g. 1ms)
        return 500 - (speedVal * 4.9);
    }

    resetArray() {
        if (this.isSorting) this.reset();

        this.container.innerHTML = '';
        this.array = [];
        this.bars = [];

        for (let i = 0; i < this.size; i++) {
            // Random Height between 5% and 95%
            const val = randomIntFromInterval(5, 95);
            this.array.push(val);

            const bar = document.createElement('div');
            bar.classList.add('array-bar');
            bar.style.height = `${val}%`;
            bar.style.width = `${60 / this.size}vw`;

            this.container.appendChild(bar);
            this.bars.push(bar);
        }
    }

    selectAlgo(btn) {
        // Clear previous active
        this.algoBtns.forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        this.currentAlgo = btn.dataset.algo;
        this.startVisualization();
    }

    async startVisualization() {
        this.isSorting = true;
        this.isPaused = false;
        this.setControlsDisabled(true);
        this.pauseBtn.disabled = false;
        this.pauseBtn.textContent = 'Pause';
        this.resetBtn.disabled = false;

        // Create a copy for the algorithm
        const arrayCopy = this.array.slice();
        let animations = [];

        switch (this.currentAlgo) {
            case 'merge':
                animations = getMergeSortAnimations(arrayCopy);
                break;
            case 'quick':
                animations = getQuickSortAnimations(arrayCopy);
                break;
            case 'heap':
                animations = getHeapSortAnimations(arrayCopy);
                break;
            case 'bubble':
                animations = getBubbleSortAnimations(arrayCopy);
                break;
        }

        this.abortController = new AbortController();
        try {
            await this.animate(animations, this.abortController.signal);
            if (!this.abortController.signal.aborted) {
                this.onFinished();
            }
        } catch (e) {
            console.log('Visualization aborted');
        }
    }

    async animate(animations, signal) {
        for (let i = 0; i < animations.length; i++) {
            if (signal.aborted) return;

            // Handle Pause
            while (this.isPaused) {
                if (signal.aborted) return;
                await sleep(100);
            }

            const [type, v1, v2, v3, v4] = animations[i];
            const barOneStyle = this.bars[v1].style;

            if (type === 'compare') {
                const barTwoStyle = this.bars[v2].style;
                barOneStyle.backgroundColor = SECONDARY_COLOR;
                barTwoStyle.backgroundColor = SECONDARY_COLOR;
            } else if (type === 'revert') {
                const barTwoStyle = this.bars[v2].style;
                barOneStyle.backgroundColor = PRIMARY_COLOR;
                barTwoStyle.backgroundColor = PRIMARY_COLOR;
            } else if (type === 'swap') {
                const idx1 = v1;
                const h1 = v2;
                const idx2 = v3;
                const h2 = v4;
                const barTwoStyle = this.bars[idx2].style;

                barOneStyle.height = `${h1}%`;
                barTwoStyle.height = `${h2}%`;
            } else if (type === 'overwrite') {
                barOneStyle.height = `${v2}%`;
                barOneStyle.backgroundColor = SWAP_COLOR;

                setTimeout(() => {
                    if (!this.isSorting) return;
                    barOneStyle.backgroundColor = PRIMARY_COLOR;
                }, this.delay || 10);
            }

            // Yield to keep UI responsive
            if (this.delay === 0) {
                if (i % 5 === 0) await sleep(0);
            } else {
                await sleep(this.delay);
            }
        }
    }

    pause() {
        this.isPaused = true;
        this.pauseBtn.textContent = 'Resume';
    }

    resume() {
        this.isPaused = false;
        this.pauseBtn.textContent = 'Pause';
    }

    reset() {
        if (this.abortController) {
            this.abortController.abort();
        }
        this.isSorting = false;
        this.isPaused = false;
        this.setControlsDisabled(false);
        this.pauseBtn.textContent = 'Pause';
        this.pauseBtn.disabled = true;
        this.resetBtn.disabled = true;

        // Clear colors
        for (const bar of this.bars) {
            bar.style.backgroundColor = PRIMARY_COLOR;
        }
        this.algoBtns.forEach(b => b.classList.remove('active'));

        this.resetArray();
    }

    onFinished() {
        this.isSorting = false;
        this.setControlsDisabled(false);
        this.pauseBtn.disabled = true;
        this.resetBtn.disabled = true;
        this.algoBtns.forEach(b => b.classList.remove('active'));

        // Turn all green
        this.bars.forEach(bar => bar.style.backgroundColor = SORTED_COLOR);
    }

    setControlsDisabled(disabled) {
        this.genBtn.disabled = disabled;
        this.sizeSlider.disabled = disabled;
        this.algoBtns.forEach(b => {
            b.disabled = disabled;
        });
    }
}

// Initialize
const visualizer = new Visualizer();

The key concepts here:

  • State management: isSorting, isPaused, etc. keep track of what's happening
  • AbortController: Lets us cancel the visualization mid-run (super important for the reset button)
  • Async/await: The animate() function uses async/await with sleep() to pause between animations without blocking the UI
  • Event listeners: Everything is wired up so buttons actually do things
  • Color coding: Different operations get different colors so you can see what's happening

  • Part 6: Key Implementation Details (aka "The Gotchas")

    Why Copy the Array?

    const arrayCopy = this.array.slice();
    animations = getMergeSortAnimations(arrayCopy);

If we pass this.array directly, the sorting algorithm will modify it immediately (since sorting happens in-place). We want the animations to show the sorting process, not the final result instantly. By copying, we let the algorithm run on a separate array while we replay the animations.

The Animation Protocol

Why return an array of animations instead of just animating directly? Because:

  1. Separation of concerns: Algorithms don't care about DOM
  2. Flexibility: We can replay animations, adjust speed, pause/resume
  3. Testing: We can test algorithm correctness independently from visualization
  4. Pause/Resume with AbortController

    this.abortController = new AbortController();
    // ...
    while (this.isPaused) {
        if (signal.aborted) return;
        await sleep(100);
    }

This lets us pause the animation gracefully. The signal is checked so if the user resets, we can abort the entire animation loop.


Part 7: Lessons Learned (aka "What I'd Do Differently")

  1. Colors could be customizable: Let users pick their own color scheme
  2. More algorithms: Add Shell Sort, Insertion Sort, Counting Sort
  3. Statistics display: Show comparisons/swaps count in real-time
  4. Sound effects: Every comparison = a little beep (for masochists)
  5. Algorithm comparison: Run multiple algorithms side-by-side
  6. Mobile gestures: Swipe to change algorithms, pinch to resize

  7. Conclusion

Building this sorting visualizer taught me a lot about:

  • Async JavaScript and proper UI responsiveness
  • State management without a framework
  • How satisfying it is to watch algorithms work
  • That at 2 AM, I apparently become a mad scientist

The whole project is vanilla JavaScript with zero dependencies. No React, no Vue, no Angular. Just HTML, CSS, and JavaScript doing what it does best.

If you want to use this, just clone the repo, open index.html in your browser, and watch computers do what computers love: organizing stuff.

Happy sorting! 🎨


Want to build something similar? Start with the HTML structure, add some CSS styling, then implement ONE algorithm at a time. The hardest part is getting the animation protocol right—everything else is just following the pattern.

Now if you'll excuse me, I need to go watch my bars sort themselves. Again. For the hundredth time. Send help (or more sorting algorithms).

#JavaScript#Algorithms#Visualization