Bubble Sort Visualizer in Rust + WASM
Compile Rust to WebAssembly and animate Bubble Sort in the browser.
Introduction
30 Second Summary
Sorting algorithms are one of those foundational computer science topics you hear about everywhere. Reading about them in a textbook never clicks the same way as watching them happen in real time.
In this project, you will build a browser-based Bubble Sort visualizer. You'll write the sorting logic in Rust, compile it to WebAssembly, and render animated bars on an HTML Canvas that swap in real time as the algorithm executes.
What You'll Build
A web app that animates Bubble Sort step-by-step, with the algorithm logic written in Rust and compiled to WebAssembly for the browser.
By the end of this project, you'll have:
- A Rust library that implements Bubble Sort with step-by-step state capture.
- WASM bindings that expose sort steps to JavaScript via wasm-bindgen.
- An HTML Canvas frontend that renders animated bars swapping in real time, with a text explanation panel describing each comparison.
- Secret Mission: Add a Marathi/English language toggle for the explanation text.
Are there any prerequisites?
This project assumes you have Rust and Cargo already installed on your machine, along with IntelliJ IDEA with the Rust plugin. Basic programming knowledge is all you need. A Java background is more than sufficient.
Set Up the Project
Your Bubble Sort visualizer has two halves: a Rust library that runs the sorting algorithm, and an HTML page that displays the results in the browser.
In this step, you'll scaffold a new Rust library project, configure its dependencies for WebAssembly compilation, and create the HTML frontend with a canvas and playback controls.
In this step, get ready to:
- Create a Rust library project with Cargo and configure its WASM dependencies.
- Create the HTML frontend with a canvas element and playback controls.
- Verify the project compiles to WebAssembly.
Create the Rust library project
Rust libraries are the building block for wasm-pack projects. You need a library crate (not a binary) because wasm-pack compiles library code into a .wasm module that JavaScript can import.
- Open your terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a new Rust library project and navigate into it by running these commands:
cargo new --lib bubble-sort-viz
cd bubble-sort-viz
- Confirm the project was created by running:
ls src
You should see lib.rs listed in the output.
What did cargo new --lib create?
This command scaffolds a Rust library project with two files: Cargo.toml (the project manifest that lists dependencies and build settings) and src/lib.rs (the source code entry point).
The --lib flag tells Cargo to create a library instead of a binary executable. Libraries expose functions that other programs (in this case, JavaScript) can call.
Permission denied or command not found?
If you see a permission error, make sure your terminal has write access to your Desktop. If cargo is not found, confirm Rust is installed by running rustc --version.
Help me fix a cargo command error when creating my Rust project.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ and press Enter to open it.
- Click Open (or File > Open if a project is already open).
- Navigate to your Desktop and select the bubble-sort-viz folder, then click OK.
IntelliJ opens the project and indexes your Rust files automatically.
- In the Project panel on the left, expand src and open lib.rs.
- Select all the default code and delete it, leaving the file completely empty.
- Save lib.rs with Cmd+S (macOS) or Ctrl+S (Windows).
Why clear lib.rs?
Cargo generates placeholder code (a sample add function and test). You'll replace it with your own Bubble Sort implementation in the next step, so clearing it now keeps things clean.
- In the Project panel, open Cargo.toml.
- Replace the entire contents of the file with the following configuration:
[package]
name = "bubble-sort-viz"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.126"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.web-sys]
version = "0.3.103"
features = ["console"]
What does this configuration do?
- crate-type = ["cdylib"] tells Rust to produce a C-compatible dynamic library. This is the format wasm-pack needs to generate a .wasm file.
- wasm-bindgen creates the bridge between your Rust functions and JavaScript. It generates glue code that lets JS call Rust and vice versa.
- serde and serde_json handle serialization. You'll use them to pass complex data (like sort step arrays) across the WASM boundary as JSON strings.
- web-sys with the console feature gives your Rust code access to the browser's developer console for debugging.
- Save Cargo.toml with Cmd+S (macOS) or Ctrl+S (Windows).
IntelliJ showing red underlines in Cargo.toml?
If IntelliJ highlights the dependency versions in red, it may still be downloading the crate index. Wait a moment for IntelliJ to finish syncing. You can also click the refresh icon in the Cargo tool window to trigger a manual sync.
If errors persist, check that you copied the file exactly as shown, including the quotes around version numbers.
Help me fix a Cargo.toml error in my wasm-pack project.
Create the HTML frontend file
Your visualizer needs an HTML page with a Canvas element for drawing bars, a text panel for step explanations, and buttons to control playback.
- Right-click the bubble-sort-viz project root in IntelliJ's Project panel.
- Select New > File.
- Name it index.html and press Enter.
- Paste the following HTML into the new file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bubble Sort Visualizer - Rust + WASM</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
h1 {
margin-bottom: 20px;
color: #e94560;
}
#canvas {
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
}
#explanation {
margin-top: 16px;
padding: 16px 24px;
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
font-size: 1.1rem;
min-height: 50px;
width: 600px;
text-align: center;
}
.controls {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
button:hover {
background: #c73e54;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.speed-control label {
font-size: 0.9rem;
color: #aaa;
}
#speed {
width: 100px;
}
#step-counter {
font-size: 0.9rem;
color: #aaa;
margin-top: 8px;
}
</style>
</head>
<body>
<h1>Bubble Sort Visualizer</h1>
<canvas id="canvas" width="600" height="300"></canvas>
<div id="explanation">Click "Start" to begin sorting!</div>
<div id="step-counter"></div>
<div class="controls">
<button id="start-btn">Start</button>
<button id="pause-btn">Pause</button>
<button id="reset-btn">Reset</button>
<div class="speed-control">
<label for="speed">Speed:</label>
<input type="range" id="speed" min="50" max="1000" value="300">
</div>
</div>
</body>
</html>
What does this HTML set up?
- A 600x300 <canvas> element where you'll draw the sorting bars in later steps.
- An #explanation div that will display what the algorithm is doing at each step.
- Start, Pause, and Reset buttons for playback control.
- A speed slider (range 50 to 1000) to control how fast the animation plays.
- Dark theme styling to make the colored sorting bars stand out visually.
- Save index.html with Cmd+S (macOS) or Ctrl+S (Windows).
- Verify the project compiles by switching to your terminal and running this command from the bubble-sort-viz directory:
wasm-pack build --target web
You should see output ending with a success message and a new pkg directory created in your project folder.
What does wasm-pack build --target web do?
This command compiles your Rust code to a .wasm binary and generates JavaScript glue code in a pkg folder. The --target web flag produces ES module output that you can import directly in the browser with a <script type="module"> tag.
Build failed?
Make sure you are running the command from inside the bubble-sort-viz directory. Run pwd to check your current location.
If you see an error about a missing target, confirm the wasm32-unknown-unknown target is installed by running rustup target list --installed. You should see wasm32-unknown-unknown in the list.
Help me fix a wasm-pack build error in my Rust project.
✔️ Awesome, I've got everything!
Great. Double check you've saved both Cargo.toml and index.html before moving on.
ⓧ I'd like to double check the full code
Here is the full Cargo.toml you should have at this point:
[package]
name = "bubble-sort-viz"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.126"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.web-sys]
version = "0.3.103"
features = ["console"]
Here is the full index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bubble Sort Visualizer - Rust + WASM</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
h1 {
margin-bottom: 20px;
color: #e94560;
}
#canvas {
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
}
#explanation {
margin-top: 16px;
padding: 16px 24px;
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
font-size: 1.1rem;
min-height: 50px;
width: 600px;
text-align: center;
}
.controls {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
button:hover {
background: #c73e54;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.speed-control label {
font-size: 0.9rem;
color: #aaa;
}
#speed {
width: 100px;
}
#step-counter {
font-size: 0.9rem;
color: #aaa;
margin-top: 8px;
}
</style>
</head>
<body>
<h1>Bubble Sort Visualizer</h1>
<canvas id="canvas" width="600" height="300"></canvas>
<div id="explanation">Click "Start" to begin sorting!</div>
<div id="step-counter"></div>
<div class="controls">
<button id="start-btn">Start</button>
<button id="pause-btn">Pause</button>
<button id="reset-btn">Reset</button>
<div class="speed-control">
<label for="speed">Speed:</label>
<input type="range" id="speed" min="50" max="1000" value="300">
</div>
</div>
</body>
</html>
Your src/lib.rs should be empty at this point. You'll add the sorting logic in the next step.
Your project skeleton is ready. Next up, you'll write the Bubble Sort algorithm in Rust with step-by-step state capture so every comparison and swap gets recorded for the visualizer.
Implement Bubble Sort with State Capture in Rust
Your project is scaffolded, compiled, and ready for real code. Now it is time to write the algorithm that powers the entire visualizer.
Bubble Sort works by repeatedly comparing adjacent elements and swapping them if they are out of order. To visualize this process, you need to capture a snapshot of the array at every single comparison. Each snapshot becomes a "frame" in your animation later.
In this step, get ready to:
- Define a struct that captures the state of the array at each comparison.
- Implement Bubble Sort so it records every step instead of just returning a sorted result.
- Expose your Rust functions to JavaScript through WebAssembly bindings.
Define the SortStep struct
Every animation needs frames. In your visualizer, each frame is a snapshot of the array at a single comparison point. You will define a struct that holds all the information one frame needs.
- In IntelliJ, open src/lib.rs and replace all of its contents with the following imports and struct definition:
use serde::Serialize;
use wasm_bindgen::prelude::*;
// Each SortStep captures one "frame" of the sorting animation
#[derive(Serialize, Clone)]
pub struct SortStep {
pub array: Vec<u32>,
pub comparing: (usize, usize),
pub swapped: bool,
pub explanation: String,
}
What does this code do?
The two use statements import serde's Serialize trait (for converting structs to JSON) and wasm-bindgen's prelude (for exposing Rust to JavaScript).
The SortStep struct has four fields. array stores the full array snapshot after a potential swap. comparing holds the two indices being compared. swapped records whether a swap happened. explanation holds a human-readable description of this step.
- Save src/lib.rs.
- You won't see a visible change yet. The next chunk adds the function that populates these structs.
Implement the bubble_sort_steps function
A normal Bubble Sort just returns a sorted array. Your version is different. It records every comparison as a SortStep, building up a complete timeline that the frontend can replay.
- In src/lib.rs, add the following function below the SortStep struct:
#[wasm_bindgen]
pub fn bubble_sort_steps(input: &str) -> String {
let mut arr: Vec<u32> = serde_json::from_str(input).unwrap_or_default();
let mut steps: Vec<SortStep> = Vec::new();
let n = arr.len();
for i in 0..n {
for j in 0..n - 1 - i {
let swapped = arr[j] > arr[j + 1];
let explanation = if swapped {
format!(
"Comparing index {} and {}: {} > {}, swapping!",
j, j + 1, arr[j], arr[j + 1]
)
} else {
format!(
"Comparing index {} and {}: {} <= {}, no swap needed.",
j, j + 1, arr[j], arr[j + 1]
)
};
if swapped {
arr.swap(j, j + 1);
}
steps.push(SortStep {
array: arr.clone(),
comparing: (j, j + 1),
swapped,
explanation,
});
}
}
serde_json::to_string(&steps).unwrap_or_default()
}
What does this code do?
The function accepts a JSON string (like "[5, 3, 8, 1]") and deserializes it into a Vec<u32>. It then runs the classic Bubble Sort double loop.
At every comparison, before moving on, it builds an explanation string and pushes a SortStep onto the steps vector. The explanation changes depending on whether a swap happened.
At the end, it serializes the entire steps vector back to a JSON string. JavaScript will parse this JSON to drive the animation frame by frame.
Why serialize to JSON instead of returning a Rust struct?
WebAssembly can only pass simple types (numbers, strings) across the boundary between Rust and JavaScript. Complex structs like Vec<SortStep> cannot cross directly.
Serializing to JSON with serde_json gives you a plain string that crosses the boundary cleanly. JavaScript then uses JSON.parse() on its side to get a regular array of objects.
- Save src/lib.rs.
- You won't see a change yet. The next chunk adds a helper function, and then you will compile everything.
Expose functions to JavaScript with wasm_bindgen
Your visualizer needs an initial array to sort. You will add a helper function that generates a pseudo-random array of numbers, also exposed to JavaScript through the same #[wasm_bindgen] annotation pattern.
- In src/lib.rs, add the following function below bubble_sort_steps:
#[wasm_bindgen]
pub fn generate_random_array(size: usize, max_val: u32) -> String {
let arr: Vec<u32> = (0..size)
.map(|i| ((i * 7 + 3) % max_val as usize) as u32 + 1)
.collect();
serde_json::to_string(&arr).unwrap_or_default()
}
What does this code do?
This function generates a deterministic array of numbers using a simple formula. It avoids the rand crate, which requires extra configuration to compile to WebAssembly.
The #[wasm_bindgen] annotation tells the compiler to generate JavaScript bindings for this function. Any function with this annotation becomes callable from your HTML page's JavaScript code.
- Save src/lib.rs.
- Compile your Rust code to WebAssembly by running this command in the terminal:
wasm-pack build --target web
What does this command do?
This compiles your Rust library to a .wasm binary and generates a JavaScript wrapper module. The --target web flag tells wasm-pack to output ES modules you can import directly in a <script type="module"> tag.
The output goes into a pkg/ directory containing bubble_sort_viz.js (the JS wrapper) and bubble_sort_viz_bg.wasm (the compiled binary).
- You should see output ending with a line like [INFO]: Your wasm pkg is ready to publish.
- Confirm the pkg/ folder was created by running:
ls pkg
- You should see bubble_sort_viz.js, bubble_sort_viz_bg.wasm, and several other files in the output.
Build failed with an error?
If you see a can't find crate error, make sure your Cargo.toml dependencies match exactly what you set up in Step 1. Double check that wasm-bindgen, serde, and serde_json are all listed.
If you see a target-related error, confirm you added the WASM target by running rustup target list --installed and checking that wasm32-unknown-unknown appears.
help me fix my wasm-pack build error
✔️ Awesome, I've got everything!
Great. Double check you have saved src/lib.rs before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/lib.rs file as it should look at the end of this step:
use serde::Serialize;
use wasm_bindgen::prelude::*;
#[derive(Serialize, Clone)]
pub struct SortStep {
pub array: Vec<u32>,
pub comparing: (usize, usize),
pub swapped: bool,
pub explanation: String,
}
#[wasm_bindgen]
pub fn bubble_sort_steps(input: &str) -> String {
let mut arr: Vec<u32> = serde_json::from_str(input).unwrap_or_default();
let mut steps: Vec<SortStep> = Vec::new();
let n = arr.len();
for i in 0..n {
for j in 0..n - 1 - i {
let swapped = arr[j] > arr[j + 1];
let explanation = if swapped {
format!(
"Comparing index {} and {}: {} > {}, swapping!",
j, j + 1, arr[j], arr[j + 1]
)
} else {
format!(
"Comparing index {} and {}: {} <= {}, no swap needed.",
j, j + 1, arr[j], arr[j + 1]
)
};
if swapped {
arr.swap(j, j + 1);
}
steps.push(SortStep {
array: arr.clone(),
comparing: (j, j + 1),
swapped,
explanation,
});
}
}
serde_json::to_string(&steps).unwrap_or_default()
}
#[wasm_bindgen]
pub fn generate_random_array(size: usize, max_val: u32) -> String {
let arr: Vec<u32> = (0..size)
.map(|i| ((i * 7 + 3) % max_val as usize) as u32 + 1)
.collect();
serde_json::to_string(&arr).unwrap_or_default()
}
Your Rust algorithm is compiled and ready to run in the browser. Next up, you will wire the WASM module into JavaScript and render animated bars on the Canvas.
Wire Up WASM to JavaScript and Render Bars on Canvas
Your Rust library captures every comparison and swap as a SortStep, serialized to JSON and ready to cross the WebAssembly boundary.
Now you need a JavaScript module that loads the WASM binary, feeds it an array, and translates the returned steps into animated bars on a Canvas element. By the end of this step, clicking Start will animate the full Bubble Sort in your browser.
In this step, get ready to:
- Write the JavaScript module that imports and initializes the WASM package.
- Implement a Canvas rendering function that draws scaled bars for each array element.
- Create an animation loop that steps through each sort state and updates the display.
Write the JavaScript module that imports the WASM package
ES modules let you import the WASM package's JavaScript glue file directly in the browser. The init() function fetches and compiles the .wasm binary, making all your exported Rust functions available to JavaScript.
- In index.html, find the closing </body> tag. Add the following <script type="module"> block just above it:
<script type="module">
// Import the WASM init function and our exported Rust functions
import init, { bubble_sort_steps, generate_random_array } from './pkg/bubble_sort_viz.js';
// State variables to track the sort animation
let steps = [];
let currentStep = 0;
let isPlaying = false;
let animationTimeout = null;
let currentArray = [];
// Cache references to DOM elements
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const explanation = document.getElementById('explanation');
const stepCounter = document.getElementById('step-counter');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const speedSlider = document.getElementById('speed');
// Initialize WASM and generate the first array
async function main() {
await init();
resetArray();
}
// Generate a fresh random array and reset all playback state
function resetArray() {
const arrayJson = generate_random_array(20, 100);
currentArray = JSON.parse(arrayJson);
steps = [];
currentStep = 0;
isPlaying = false;
clearTimeout(animationTimeout);
drawBars(currentArray, -1, -1);
explanation.textContent = 'Click "Start" to begin sorting!';
stepCounter.textContent = '';
}
What does this code do?
The import line pulls in three things from the WASM glue file: init (loads the binary), bubble_sort_steps (runs the sort), and generate_random_array (creates a shuffled array).
The main() function calls await init() to compile the WASM module, then resetArray() generates 20 values (max 100) and draws the initial bar chart. All state is stored in module-level variables so event handlers can access it later.
You won't see a change yet. The next chunk adds the drawBars() function that resetArray() calls.
Implement the Canvas rendering function
The Canvas 2D API gives you pixel-level control over drawing. Each array element becomes a rectangle whose height is proportional to its value, and the two elements currently being compared get highlighted in a contrasting colour.
- Directly below the resetArray() function you just added, paste the following drawBars function:
// Draw the array as vertical bars on the canvas
function drawBars(array, idx1, idx2) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / array.length;
const maxVal = Math.max(...array);
for (let i = 0; i < array.length; i++) {
const barHeight = (array[i] / maxVal) * (canvas.height - 20);
const x = i * barWidth;
const y = canvas.height - barHeight;
// Highlight the two bars being compared
if (i === idx1 || i === idx2) {
ctx.fillStyle = '#e94560';
} else {
ctx.fillStyle = '#533483';
}
ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
// Draw the numeric value below each bar
ctx.fillStyle = '#eee';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(array[i], x + barWidth / 2, canvas.height - 4);
}
}
What does this code do?
The function clears the entire canvas, then loops through the array drawing one rectangle per element. Bar height is scaled relative to the largest value so the tallest bar always fills the canvas.
When idx1 or idx2 matches the current index, that bar turns red (#e94560). Otherwise it stays purple (#533483). Passing -1 for both indices means nothing is highlighted, which is what resetArray() does.
You won't see a change yet. The next chunk adds the animation loop and event wiring that brings everything to life.
Create the animation loop and wire up the buttons
The animation works by stepping through the array of SortStep objects one at a time. Each step redraws the bars and updates the explanation text. The speed slider controls the delay between frames.
- Below the drawBars function, paste the animation loop, event listeners, and the main() call. Then close the script tag:
// Advance one step in the sort animation
function playStep() {
if (currentStep >= steps.length) {
isPlaying = false;
explanation.textContent = 'Sorting complete!';
return;
}
const step = steps[currentStep];
drawBars(step.array, step.comparing[0], step.comparing[1]);
explanation.textContent = step.explanation;
stepCounter.textContent = `Step ${currentStep + 1} of ${steps.length}`;
currentStep++;
if (isPlaying) {
const delay = 1050 - parseInt(speedSlider.value);
animationTimeout = setTimeout(playStep, delay);
}
}
// Start button: generate steps from the current array and begin playback
startBtn.addEventListener('click', () => {
if (steps.length === 0) {
const inputJson = JSON.stringify(currentArray);
const stepsJson = bubble_sort_steps(inputJson);
steps = JSON.parse(stepsJson);
currentStep = 0;
}
isPlaying = true;
playStep();
});
// Pause button: stop the animation loop
pauseBtn.addEventListener('click', () => {
isPlaying = false;
clearTimeout(animationTimeout);
});
// Reset button: generate a new array and clear playback
resetBtn.addEventListener('click', resetArray);
main();
</script>
What does this code do?
The playStep() function reads the current SortStep, redraws the canvas with the two compared indices highlighted, and updates the explanation panel. If playback is active, it schedules itself again after a delay.
The delay formula 1050 - parseInt(speedSlider.value) inverts the slider so that dragging right (higher value) makes the animation faster. When the slider is at 300 (default), each frame waits 750ms.
The Start button calls bubble_sort_steps(inputJson) only on the first click, caching the result so Pause/Resume doesn't regenerate steps.
Why setTimeout instead of requestAnimationFrame?
The speed slider needs to control how long the viewer sees each comparison. With requestAnimationFrame you get ~60fps with no built-in throttle. Using setTimeout with a variable delay gives direct control over pacing, which is essential for an educational visualizer.
- Save index.html.
- ES modules require an HTTP server. Start one from your project root by running this command:
python3 -m http.server
- Open your browser and navigate to http://localhost:8000.
- You should see 20 purple bars of varying heights on the dark canvas, with numeric labels along the bottom.
- Click Start. The bars should animate, highlighting pairs in red as they compare and swap.
Seeing a blank canvas or console errors?
- If the browser shows a CORS or module error, make sure you're using python3 -m http.server and accessing http://localhost:8000. Opening the file directly (file://) will not work with ES modules.
- If you see an import error for bubble_sort_viz.js, confirm that the pkg/ folder exists in the same directory as index.html. If not, run wasm-pack build --target web again from the project root.
- If bars appear but clicking Start does nothing, open DevTools (F12) and check the Console tab for error messages.
Still stuck? Help me debug my WASM import or Canvas rendering issue.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and the animation plays all the way through to "Sorting complete!".
ⓧ I'd like to double check the full code
Here is the complete index.html including the full script block:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bubble Sort Visualizer - Rust + WASM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #eee; display: flex; flex-direction: column; align-items: center; padding: 20px; min-height: 100vh; }
h1 { margin-bottom: 20px; color: #e94560; }
#canvas { background: #16213e; border-radius: 8px; border: 2px solid #0f3460; }
#explanation { margin-top: 16px; padding: 16px 24px; background: #16213e; border-radius: 8px; border: 2px solid #0f3460; font-size: 1.1rem; min-height: 50px; width: 600px; text-align: center; }
.controls { margin-top: 16px; display: flex; gap: 12px; align-items: center; }
button { padding: 10px 20px; background: #e94560; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; transition: background 0.2s; }
button:hover { background: #c73e54; }
.speed-control { display: flex; align-items: center; gap: 8px; }
.speed-control label { font-size: 0.9rem; color: #aaa; }
#speed { width: 100px; }
#step-counter { font-size: 0.9rem; color: #aaa; margin-top: 8px; }
</style>
</head>
<body>
<h1>Bubble Sort Visualizer</h1>
<canvas id="canvas" width="600" height="300"></canvas>
<div id="explanation">Click "Start" to begin sorting!</div>
<div id="step-counter"></div>
<div class="controls">
<button id="start-btn">Start</button>
<button id="pause-btn">Pause</button>
<button id="reset-btn">Reset</button>
<div class="speed-control">
<label for="speed">Speed:</label>
<input type="range" id="speed" min="50" max="1000" value="300">
</div>
</div>
<script type="module">
import init, { bubble_sort_steps, generate_random_array } from './pkg/bubble_sort_viz.js';
let steps = [];
let currentStep = 0;
let isPlaying = false;
let animationTimeout = null;
let currentArray = [];
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const explanation = document.getElementById('explanation');
const stepCounter = document.getElementById('step-counter');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const speedSlider = document.getElementById('speed');
async function main() {
await init();
resetArray();
}
function resetArray() {
const arrayJson = generate_random_array(20, 100);
currentArray = JSON.parse(arrayJson);
steps = [];
currentStep = 0;
isPlaying = false;
clearTimeout(animationTimeout);
drawBars(currentArray, -1, -1);
explanation.textContent = 'Click "Start" to begin sorting!';
stepCounter.textContent = '';
}
function drawBars(array, idx1, idx2) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / array.length;
const maxVal = Math.max(...array);
for (let i = 0; i < array.length; i++) {
const barHeight = (array[i] / maxVal) * (canvas.height - 20);
const x = i * barWidth;
const y = canvas.height - barHeight;
if (i === idx1 || i === idx2) {
ctx.fillStyle = '#e94560';
} else {
ctx.fillStyle = '#533483';
}
ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
ctx.fillStyle = '#eee';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(array[i], x + barWidth / 2, canvas.height - 4);
}
}
function playStep() {
if (currentStep >= steps.length) {
isPlaying = false;
explanation.textContent = 'Sorting complete!';
return;
}
const step = steps[currentStep];
drawBars(step.array, step.comparing[0], step.comparing[1]);
explanation.textContent = step.explanation;
stepCounter.textContent = `Step ${currentStep + 1} of ${steps.length}`;
currentStep++;
if (isPlaying) {
const delay = 1050 - parseInt(speedSlider.value);
animationTimeout = setTimeout(playStep, delay);
}
}
startBtn.addEventListener('click', () => {
if (steps.length === 0) {
const inputJson = JSON.stringify(currentArray);
const stepsJson = bubble_sort_steps(inputJson);
steps = JSON.parse(stepsJson);
currentStep = 0;
}
isPlaying = true;
playStep();
});
pauseBtn.addEventListener('click', () => {
isPlaying = false;
clearTimeout(animationTimeout);
});
resetBtn.addEventListener('click', resetArray);
main();
</script>
</body>
</html>
Your visualizer is alive. You can watch Bubble Sort compare and swap elements in real-time, powered by Rust logic running at near-native speed in the browser. Next up, you'll polish the experience with playback controls and a step-by-step explanation panel.
Add the Explanation Panel and Playback Controls
Your visualizer already draws animated bars and steps through the algorithm. Now it's time to make the experience polished and interactive.
In this step, you'll expand the CSS into a readable format, understand how the playback controls drive the animation, and verify that the Reset button properly regenerates a fresh array.
In this step, get ready to:
- Style the explanation panel with readable, expanded CSS.
- Verify the Play/Pause button and speed slider control the animation.
- Test the Reset button to confirm it generates a new random array.
Expand the explanation panel and control styles
Your index.html currently has all the CSS rules compressed into single lines. Expanding them into a readable format makes the styles easier to maintain and debug.
- In index.html, find the <style> block inside <head>.
- Replace the entire <style> block with the expanded version below:
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
h1 {
margin-bottom: 20px;
color: #e94560;
}
#canvas {
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
}
#explanation {
margin-top: 16px;
padding: 16px 24px;
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
font-size: 1.1rem;
min-height: 50px;
width: 600px;
text-align: center;
}
.controls {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
button:hover {
background: #c73e54;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.speed-control label {
font-size: 0.9rem;
color: #aaa;
}
#speed {
width: 100px;
}
#step-counter {
font-size: 0.9rem;
color: #aaa;
margin-top: 8px;
}
</style>
What do the explanation panel styles do?
The #explanation selector targets the div that displays step-by-step text like "Comparing index 2 and 3: 7 > 4, swapping!" during the animation.
The min-height: 50px prevents the panel from collapsing when it has short text. The width: 600px matches the canvas width so they align visually.
The #step-counter shows a small "Step X of Y" counter in muted gray (#aaa) so it stays informative without distracting from the main explanation.
- Save index.html.
- Refresh your browser. The explanation panel below the canvas should have a dark background (#16213e), a subtle border, and the text "Click "Start" to begin sorting!" centered inside.
Panel not showing?
Check that the #explanation CSS rule has a closing brace. A missing } will cascade errors to every rule below it.
Make sure the HTML still has <div id="explanation"> between the canvas and the controls div.
Help me debug my explanation panel styling.
Verify the Play/Pause and speed controls
The playback controls are already wired up in your JavaScript. The Play button triggers playStep() in a loop, while Pause stops it by clearing the scheduled timeout.
- In your browser, click the Start button.
- Watch the bars animate and the explanation text update with each comparison.
- Click Pause to freeze the animation mid-sort.
- Drag the Speed slider to the right (faster) and click Start again to resume at a faster pace.
How does the speed slider work?
The slider ranges from 50 to 1000 with a default of 300. The animation delay is calculated as 1050 - sliderValue milliseconds.
When the slider is at 50 (leftmost), the delay is 1000ms (slow). When the slider is at 1000 (rightmost), the delay is 50ms (fast). This inversion makes "drag right = faster" feel intuitive.
You should see the step counter update as "Step 1 of 171" (or similar) and the explanation panel display messages like "Comparing index 2 and 3: 7 > 4, swapping!" for each comparison.
Animation not starting?
Open the browser console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and check for errors. A common issue is the local HTTP server not running. ES modules require a server to load.
Make sure your local server is still running in the terminal (e.g. python3 -m http.server) from the project root.
Help me debug why my bubble sort animation isn't starting.
Test the Reset button
The Reset button calls resetArray(), which generates a brand new array through the WASM function, clears all animation state, and redraws the bars from scratch.
- Let the sort run partway through (or let it finish entirely).
- Click Reset.
- The bars should redraw with new values, the explanation text should reset to "Click "Start" to begin sorting!", and the step counter should clear.
What does resetArray() do?
- generate_random_array(20, 100) calls into WASM to produce a fresh 20-element array with values between 1 and 100.
- steps, currentStep, and isPlaying are all reset so the next Start click triggers a fresh sort.
- clearTimeout(animationTimeout) cancels any in-progress animation frame to prevent ghost steps from playing.
- Click Start again after resetting. The sort should run on the new array with fresh explanation text.
Reset not generating new bars?
If the bars look identical after clicking Reset, remember that generate_random_array uses a deterministic formula. The array will always be the same set of values. What changes is that all animation state resets, letting you re-watch the sort from the beginning.
If clicking Reset does nothing at all, check that resetBtn.addEventListener('click', resetArray) exists in your script and that the button has id="reset-btn" in the HTML.
Help me troubleshoot my Reset button.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and that all three buttons (Start, Pause, Reset) work as expected.
ⓧ I'd like to double check the full code
Here is the complete index.html file. Make sure yours matches exactly.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bubble Sort Visualizer - Rust + WASM</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
h1 {
margin-bottom: 20px;
color: #e94560;
}
#canvas {
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
}
#explanation {
margin-top: 16px;
padding: 16px 24px;
background: #16213e;
border-radius: 8px;
border: 2px solid #0f3460;
font-size: 1.1rem;
min-height: 50px;
width: 600px;
text-align: center;
}
.controls {
margin-top: 16px;
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
button:hover {
background: #c73e54;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.speed-control label {
font-size: 0.9rem;
color: #aaa;
}
#speed {
width: 100px;
}
#step-counter {
font-size: 0.9rem;
color: #aaa;
margin-top: 8px;
}
</style>
</head>
<body>
<h1>Bubble Sort Visualizer</h1>
<canvas id="canvas" width="600" height="300"></canvas>
<div id="explanation">Click "Start" to begin sorting!</div>
<div id="step-counter"></div>
<div class="controls">
<button id="start-btn">Start</button>
<button id="pause-btn">Pause</button>
<button id="reset-btn">Reset</button>
<div class="speed-control">
<label for="speed">Speed:</label>
<input type="range" id="speed" min="50" max="1000" value="300">
</div>
</div>
<script type="module">
import init, { bubble_sort_steps, generate_random_array } from './pkg/bubble_sort_viz.js';
let steps = [];
let currentStep = 0;
let isPlaying = false;
let animationTimeout = null;
let currentArray = [];
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const explanation = document.getElementById('explanation');
const stepCounter = document.getElementById('step-counter');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const speedSlider = document.getElementById('speed');
async function main() {
await init();
resetArray();
}
function resetArray() {
const arrayJson = generate_random_array(20, 100);
currentArray = JSON.parse(arrayJson);
steps = [];
currentStep = 0;
isPlaying = false;
clearTimeout(animationTimeout);
drawBars(currentArray, -1, -1);
explanation.textContent = 'Click "Start" to begin sorting!';
stepCounter.textContent = '';
}
function drawBars(array, idx1, idx2) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / array.length;
const maxVal = Math.max(...array);
for (let i = 0; i < array.length; i++) {
const barHeight = (array[i] / maxVal) * (canvas.height - 20);
const x = i * barWidth;
const y = canvas.height - barHeight;
if (i === idx1 || i === idx2) {
ctx.fillStyle = '#e94560';
} else {
ctx.fillStyle = '#533483';
}
ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
ctx.fillStyle = '#eee';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(array[i], x + barWidth / 2, canvas.height - 4);
}
}
function playStep() {
if (currentStep >= steps.length) {
isPlaying = false;
explanation.textContent = 'Sorting complete!';
return;
}
const step = steps[currentStep];
drawBars(step.array, step.comparing[0], step.comparing[1]);
explanation.textContent = step.explanation;
stepCounter.textContent = `Step ${currentStep + 1} of ${steps.length}`;
currentStep++;
if (isPlaying) {
const delay = 1050 - parseInt(speedSlider.value);
animationTimeout = setTimeout(playStep, delay);
}
}
startBtn.addEventListener('click', () => {
if (steps.length === 0) {
const inputJson = JSON.stringify(currentArray);
const stepsJson = bubble_sort_steps(inputJson);
steps = JSON.parse(stepsJson);
currentStep = 0;
}
isPlaying = true;
playStep();
});
pauseBtn.addEventListener('click', () => {
isPlaying = false;
clearTimeout(animationTimeout);
});
resetBtn.addEventListener('click', resetArray);
main();
</script>
</body>
</html>
Your Bubble Sort visualizer is fully functional. You have a styled explanation panel that narrates each algorithm step, playback controls for pacing the animation, and a Reset button for starting fresh. Next up, you'll take this further with a bilingual Secret Mission.
Secret mission
Add Marathi/English Language Toggle
Your visualizer explains each sort step in English. What if learners who speak Marathi could follow along in their own language? In this mission, you'll add a language toggle that regenerates all explanation text in Marathi or English, driven entirely from Rust.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The bubble-sort-viz project folder on your Desktop (contains Cargo.toml, src/lib.rs, index.html, and the pkg/ build output).
- wasm-pack binary installed via Cargo.
- The wasm32-unknown-unknown compilation target added to your Rust toolchain.
Keep everything running
No action needed. Choose this if you plan to extend the visualizer with more sorting algorithms (selection sort, insertion sort, merge sort) or additional languages.
- Your bubble-sort-viz folder stays on your Desktop, ready to open in IntelliJ whenever you want to continue.
- wasm-pack and the wasm32-unknown-unknown target remain installed for future WASM projects.
Pause - I'll come back to this later
Stop any running local server to free up the terminal. Your project files stay on disk for when you return.
- If you still have python3 -m http.server running in a terminal, press Ctrl+C to stop it.
- Your bubble-sort-viz folder and all installed tools remain available for next time.
Delete - I don't want to use this again
Remove all project files and the tools you installed. This frees up disk space and returns your environment to its pre-project state.
- Delete the project folder by running this command:
macOS
rm -rf ~/Desktop/bubble-sort-viz
Windows
rmdir /s /q %USERPROFILE%\Desktop\bubble-sort-viz
- Uninstall wasm-pack by running:
cargo uninstall wasm-pack
- Remove the wasm32-unknown-unknown compilation target by running:
rustup target remove wasm32-unknown-unknown
That's a Wrap!
That's a Wrap!
Nice work! You just built a browser-based Bubble Sort visualizer powered entirely by Rust compiled to WebAssembly.
You've learned how to:
- Implement a Bubble Sort algorithm in Rust with step-by-step state capture that records every comparison and swap.
- Compile Rust to WebAssembly using wasm-pack and wasm-bindgen, passing complex data across the JS-WASM boundary with serde_json.
- Render animated bar charts on an HTML Canvas driven by WASM data, with real-time text explanations and playback controls.
- Secret Mission: Added bilingual Marathi/English support for explanation text, laying groundwork for a full translation system.
Ready to quiz yourself?