How to Make Interactive Art With JavaScript: A Hands-On Tutorial
You don't need Three.js, p5.js, or any framework to make interactive art with JavaScript. The browser gives you everything: a Canvas element, mouse and touch events, requestAnimationFrame, and math. That's enough to create art that responds to people.
This tutorial walks through three complete examples, each building on the last. By the end, you'll have a living, interactive artwork running in a single HTML file — no dependencies, no build step, under 5KB.
What makes art "interactive"?
Interactive art responds to input. That input could be a mouse moving, a key pressed, a screen touched, time passing, or even ambient sound. The key distinction from static generative art: the viewer's actions shape what they see. Every experience is unique because every viewer behaves differently.
The simplest form: draw something where the mouse is. Let's start there.
Example 1: A responsive particle trail
This first example creates a trail of particles that follow your mouse. Each particle has a limited lifetime and fades out, creating an organic, flowing effect.
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = innerWidth;
canvas.height = innerHeight;
const particles = [];
canvas.addEventListener('mousemove', e => {
for (let i = 0; i < 3; i++) {
particles.push({
x: e.clientX + (Math.random() - 0.5) * 20,
y: e.clientY + (Math.random() - 0.5) * 20,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2 - 1,
life: 1,
hue: (Date.now() * 0.05) % 360
});
}
});
function draw() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.01;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.life * 4, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue}, 80%, 60%, ${p.life})`;
ctx.fill();
}
requestAnimationFrame(draw);
}
draw();
</script>
Open this in a browser and move your mouse. Particles spawn at your cursor, drift upward slightly (the - 1 in vy), and fade out. The rgba(0, 0, 0, 0.05) fill creates a motion blur effect — instead of clearing the canvas each frame, we paint a nearly-transparent black rectangle, letting previous frames ghost through.
The hue shifts with time (Date.now() * 0.05), so the color palette cycles continuously. Move fast and you get bold streaks; move slowly and you get delicate clusters.
Example 2: A force field that reacts to your cursor
Now let's make something where the entire canvas responds to your position — not just where particles appear, but how a whole grid of elements behaves.
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = innerWidth;
canvas.height = innerHeight;
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
const spacing = 30;
const cols = Math.ceil(canvas.width / spacing);
const rows = Math.ceil(canvas.height / spacing);
canvas.addEventListener('mousemove', e => {
mouseX = e.clientX;
mouseY = e.clientY;
});
function draw() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * spacing + spacing / 2;
const y = j * spacing + spacing / 2;
const dx = mouseX - x;
const dy = mouseY - y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const force = Math.min(150 / dist, 1);
const len = spacing * 0.4 * (1 + force);
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(len, 0);
ctx.strokeStyle = `hsla(${dist % 360}, 70%, ${50 + force * 30}%, ${0.3 + force * 0.7})`;
ctx.lineWidth = 1 + force * 2;
ctx.stroke();
ctx.restore();
}
}
requestAnimationFrame(draw);
}
draw();
</script>
This creates a grid of small lines that all point toward your cursor, like iron filings near a magnet. The closer a line is to your mouse, the longer and brighter it becomes. The hue varies with distance, creating a natural color gradient radiating from your cursor.
Move your mouse slowly across the screen. The entire field shifts and flows. This is a vector field visualization — a concept from mathematics — turned into interactive art with about 40 lines of JavaScript.
Example 3: Generative growth with interaction
For the final example, let's build something that combines generative patterns with interaction — a branching structure that grows from wherever you click.
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const branches = [];
function addBranch(x, y, angle, len, depth, hue) {
if (depth <= 0 || len < 2) return;
branches.push({ x, y, angle, len, depth, hue, progress: 0 });
}
canvas.addEventListener('click', e => {
const hue = Math.random() * 360;
addBranch(e.clientX, e.clientY, -Math.PI / 2, 60, 8, hue);
});
function update() {
for (let i = branches.length - 1; i >= 0; i--) {
const b = branches[i];
b.progress += 0.03;
if (b.progress >= 1) {
// Branch complete — spawn children
const endX = b.x + Math.cos(b.angle) * b.len;
const endY = b.y + Math.sin(b.angle) * b.len;
const spread = 0.3 + Math.random() * 0.4;
const shrink = 0.65 + Math.random() * 0.15;
addBranch(endX, endY, b.angle - spread, b.len * shrink, b.depth - 1, b.hue + 10);
addBranch(endX, endY, b.angle + spread, b.len * shrink, b.depth - 1, b.hue + 10);
// Draw the completed branch
const alpha = 0.4 + (b.depth / 8) * 0.6;
ctx.beginPath();
ctx.moveTo(b.x, b.y);
ctx.lineTo(endX, endY);
ctx.strokeStyle = `hsla(${b.hue}, 60%, ${40 + b.depth * 5}%, ${alpha})`;
ctx.lineWidth = b.depth * 0.5;
ctx.stroke();
branches.splice(i, 1);
}
}
requestAnimationFrame(update);
}
update();
</script>
Click anywhere on the canvas and a tree-like structure grows from that point. Each branch splits into two children at slightly random angles, with shifting hues. Click multiple times and a forest emerges — each tree unique, each placement yours.
This is generative art at its core: you define the rules (branching angle, shrink factor, depth limit), and the system produces infinite variation. The interaction (where you click) gives the viewer authorship over the composition.
Combining techniques: the interactive canvas artwork
The three examples above demonstrate the fundamental building blocks of interactive art with JavaScript:
- Particle systems — objects with position, velocity, and lifetime that create organic, flowing visuals
- Force fields — grids or arrays of elements that respond to a global input (mouse position, time, audio)
- Recursive growth — structures that build themselves using simple rules, creating complex emergent forms
Real interactive artworks often combine all three. A particle system driven by a force field, growing recursive structures that emit particles, force fields that respond to generated audio — the combinations are infinite.
Making it touch-friendly
If you want your art to work on phones and tablets, add touch events alongside mouse events:
function getPosition(e) {
if (e.touches) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
return { x: e.clientX, y: e.clientY };
}
canvas.addEventListener('mousemove', e => handleMove(getPosition(e)));
canvas.addEventListener('touchmove', e => {
e.preventDefault();
handleMove(getPosition(e));
}, { passive: false });
That's all it takes. The passive: false option allows preventDefault() to stop the page from scrolling while you're drawing.
Performance tips for smooth art
Interactive art needs to run at 60fps to feel responsive. A few rules:
- Limit particle count — Set a maximum (e.g., 2000) and remove the oldest particles first when you exceed it
- Avoid
ctx.clearRect()when you can use translucent fills — The motion blur technique (rgba(0,0,0,0.05)) is both cheaper and more beautiful than clearing and redrawing everything - Pre-calculate what you can — If your grid dimensions don't change, compute cols and rows once, not every frame
- Use
ctx.beginPath()before every new shape — Forgetting this is the most common Canvas performance bug; without it, the path list grows every frame
Where to go from here
You now know enough to build interactive art with JavaScript. Here's how to push further:
- Add audio — The Web Audio API lets you generate sound from interaction. Map mouse position to frequency, particle count to volume, click intensity to percussion.
- Try WebGL shaders — Once you're comfortable with Canvas, shader art opens up a completely different visual vocabulary. Every pixel calculated mathematically, 60 times per second.
- Impose constraints — Some of the best interactive art comes from extreme constraints. Can you build something beautiful in under 50KB? Under 1KB? Under 140 characters?
- Make it collaborative — Connect multiple viewers through WebSockets and let everyone influence the same canvas. Lumitree does this at the macro level — every visitor's seed changes the tree for everyone.
The browser is the most accessible creative medium ever invented. A billion people have a Canvas element in their pocket right now. All you need is JavaScript and an idea.
Start with one of the examples above. Modify a number. Change a color. Add an event listener. The moment your code responds to you, you've crossed the line from programming into art.
Plant a seed on Lumitree and see your idea become a living, interactive micro-world — or build your own from scratch. Either way, you're making interactive art.