In this post, we will see how to create a wave effect using JavaScript. To do this, we will use the HTML5 Canvas element together with the Window.requestAnimationFrame() function, which we already explored in depth in our previous article: The secret of animations in JavaScript (requestAnimationFrame()). As a quick refresher: requestAnimationFrame() allows you to animate shapes drawn on a <canvas> element in sync with the browser's refresh cycle. In other words:
With requestAnimationFrame(), you get smooth transitions thanks to a native API optimized to work alongside Canvas. Unlike setInterval() or setTimeout(), this function integrates directly with the browser's rendering engine.
For more information about this function, check out the link above.
What is a wave effect in JavaScript and why use Canvas
A wave animation is an animation that simulates the behavior of a wave or oscillation. It can be used for a wide variety of purposes:
- Real-time data visualizations
- Animated backgrounds on landing pages
- Futuristic or artistic visual effects
- Mathematical and scientific representations
Although there are "wave" effects built purely with CSS (such as ripple buttons), the <canvas> element gives you full control over every pixel and frame. When working with Canvas:
- You draw directly onto a virtual canvas.
- You precisely control coordinates, shapes, strokes, and colors.
- You can apply real mathematics—including trigonometry—to movement.
That is why Canvas is the ideal tool for implementing sine waves and complex animations in the browser.
Why requestAnimationFrame is key for smooth animations
One of the most common mistakes when getting started with JavaScript animations is turning to setInterval() or setTimeout(). I did it myself at first… until I started noticing stuttering, excessive CPU usage, and unnatural movement.
requestAnimationFrame() solves all these problems because:
- It syncs with the monitor's refresh rate (usually 60 fps).
- It automatically pauses when the tab is inactive, saving system resources.
- It produces noticeably smoother and more natural transitions.
In wave animations, where every frame counts, the difference compared to setInterval() is huge and very noticeable.
Building the wave with JavaScript and Canvas
The JavaScript code is actually quite short and simple, though it might look a bit confusing at first glance due to the mathematical operations involved (additions, divisions, and trigonometric functions). I will explain it step by step as clearly as possible.
Before diving into the code, it is worth recalling the sine and cosine functions you likely learned in school or college. These functions describe curves that wave back and forth periodically, just like an ocean wave:

Image source: Wikipedia – Cosine.
Sine and cosine functions applied to web animations
How a sine wave behaves
The cosine or sine function is perfect for this experiment precisely because of its oscillatory behavior. If you don't remember how they work, you will find plenty of material online; the key takeaway here is that they generate values that rise and fall continuously and predictably.
Applied in JavaScript, we use them like this:
Math.cos(n)— applies the cosine function to the valuen.Math.sin(n)— applies the sine function to the valuen.
The trigonometric functions
Math.sin()andMath.cos()generate periodic curves that smoothly rise and fall between-1and1. Exactly what we need to simulate a wave.
The general formula for a sine wave is:
y = amplitude * Math.sin(frequency * x + phase);Where each parameter serves a specific purpose:
amplitude— controls the wave's maximum height (how "tall" the crest is).frequency— defines how many complete oscillations occur across the width.phase— incrementing this value in each frame generates horizontal motion.
Below is the complete JavaScript animation code:
var c = document.getElementById('canv');
var $ = c.getContext('2d');
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;
var draw = function(t) {
$.lineWidth = 1;
$.fillStyle = 'rgb(0, 0, 0)';
$.fillRect(0, 0, w, h);
for (var i = -60; i < 60; i += 1) {
$.strokeStyle = 'rgb(255, 255, 255)';
$.beginPath();
$.moveTo(0, h / 2);
for (var j = 0; j < w; j += 10) {
$.lineTo(10 * Math.cos(i) +
j + (0.008 * j * j),
Math.floor(h / 2 + j / 2 *
Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)));
};
$.stroke();
}
}
var t = 0;
window.addEventListener('resize', function() {
c.width = w = window.innerWidth;
c.height = h = window.innerHeight;
}, false);
var run = function() {
window.requestAnimationFrame(run);
t += 5;
draw(t);
};
run();As you might imagine, the HTML consists of a simple <canvas> tag with a defined id.
A few considerations regarding the code above
First, we initialize global variables to gain access to the <canvas> element, its 2D context, and the window dimensions:
var c = document.getElementById('canv');
var $ = c.getContext('2d');
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;Inside the draw() function
We define stroke styles and clear the canvas on every frame before drawing again. This step is critical: if you do not clear the canvas with fillRect() before redrawing, previous strokes will accumulate, resulting in visual clutter.
$.lineWidth = 1;
$.fillStyle = 'rgb(0, 0, 0)';
$.fillRect(0, 0, w, h);
$.strokeStyle = 'rgb(255, 255, 255)';This outer for loop draws a set of parallel lines. Changing the lower bound (-60) and upper bound (60) lets us make the waves wider or narrower. Additionally, this for loop initializes the paths required to render each line (beginPath(), moveTo(), and stroke()):
for (var i = -60; i < 60; i += 1) {
$.beginPath();
$.moveTo(0, h / 2);
/* Nested for loop */
$.stroke();
}In other words, without this outer for loop, our wave would look like a lone whip moving from side to side:

Why a single wave looks unnatural
Here is something I learned through trial and error:
when using a single sine function, the animation turns out too perfect… and for that reason, unnatural.
By combining multiple cosine functions with slight offsets and varying amplitudes, the wave begins to look more organic and fluid. That was the turning point where my animation stopped looking like a math exercise and started feeling alive.
The following snippet shows the nested for loop that draws w points per iteration, where w is the screen width in pixels:
for (var j = 0; j < w; j += 10) {
$.lineTo(10 * Math.cos(i) +
j + (0.008 * j * j),
Math.floor(h / 2 + j / 2 *
Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)));
};As you can see, we use multiple calls to Math.cos() with different arguments to create a diverse, non-monotonous wave effect. The overlapping of these curves creates that sense of natural motion.
The divisions and multiplications by small numbers—such as / 50, / 118, or * 0.9—keep values within a reasonable range and prevent the wave from distorting out of control as i and j increase.
To make the animation evolve over time, every time requestAnimationFrame() calls run() recursively, t increases by 5 units. This variable t acts as the phase that shifts the wave horizontally.
Finally, we create the run() function, which starts the main animation loop as soon as the page loads:
var run = function() {
window.requestAnimationFrame(run);
t += 5;
draw(t);
};Drawing the wave step by step: complete breakdown of draw()
function draw(t) {
ctx.lineWidth = 1;
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, w, h);
for (var i = -60; i < 60; i++) {
ctx.strokeStyle = 'rgb(255,255,255)';
ctx.beginPath();
ctx.moveTo(0, h / 2);
for (var j = 0; j < w; j += 10) {
ctx.lineTo(
10 * Math.cos(i) + j + (0.008 * j * j),
Math.floor(
h / 2 +
j / 2 * Math.cos(j / 50 - t / 50 - i / 118) +
(i * 0.9) * Math.cos(j / 25 - (i + t) / 65)
)
);
}
ctx.stroke();
}
}In short, the draw() function handles three key steps in each frame:
- The first
forloop (variablei) creates multiple parallel lines with distinct offsets. - The second
forloop (variablej) plots each line point by point across the canvas width. - Combining several
Math.cos()calls with unique arguments breaks perfect symmetry and creates organic motion.
When I removed that outer for loop during testing, the wave looked rigid like a whip. Adding it back gave the effect instant depth and volume.
Superimposing waves for an organic look
This superposition technique yields three visual qualities that a single sine wave can never produce:
- Visual variation: no two sections of the canvas look identical.
- Sense of volume: overlapping curves create an illusion of depth.
- Non-repetitive movement: the animation never feels mechanical or static.
This is precisely what simple, single sine wave examples fail to deliver.
Controlling wave amplitude, frequency, and movement
- Avoiding extreme distortions
- A common oversight is failing to constrain loop variable values. If you don't manage division and multiplication factors within the
Math.cos()arguments, the wave can grow exponentially out of control. - Pro tip: keeping divisors larger and multipliers smaller maintains a smooth, predictable animation.
- A common oversight is failing to constrain loop variable values. If you don't manage division and multiplication factors within the
- Tweakable parameters for dramatic effect
- Small changes lead to major visual updates. It is worth experimenting with:
- Incrementing the
phaseslower (reducing the+= 5step fort). - Slightly tweaking amplitude by multiplying
iby a different decimal value. - Introducing minor offsets inside
Math.cos()parameters to vary the pattern.
- Incrementing the
- Small changes lead to major visual updates. It is worth experimenting with:
- In complex canvas animations, less is often more: subtle adjustments produce the most elegant results.
Optimization and performance in Canvas animations
Common mistakes that degrade performance in a canvas wave animation:
- Redrawing without clearing the canvas first (using
fillRect()orclearRect()). - Executing excessive mathematical calculations per frame without caching intermediate results.
- Failing to update canvas dimensions on window
resizeevents.
I always set up a resize listener so the canvas stays scaled to 100% of the viewport:
window.addEventListener('resize', function () {
c.width = w = window.innerWidth;
c.height = h = window.innerHeight;
});Practical tips for stable and efficient animations
- Always prefer
requestAnimationFrame()oversetInterval(). - Avoid unnecessary loops or deeply nested logic inside
draw(). - Keep mathematical constants balanced: tuned divisors and multipliers make all the difference.
- Test across different screen sizes to verify that the canvas scales correctly.
Wave effect variations and possible enhancements
Once you master the fundamentals, the possibilities expand quickly. You can explore:
- Adding dynamic colors using
hsl(), mappingiortvalues to hue. - Introducing pseudo-random noise with
Math.random()for more expressive, chaotic visual behavior. - Creating mouse-reactive waves by tracking coordinates via
mousemoveevents. - Layering multiple wave groups at varying speeds to build depth of field.
This is where Canvas truly outshines CSS: creativity is your only boundary.
Frequently asked questions about JavaScript wave animation
- Is Canvas better than CSS for this effect?
- Yes, especially when you need true parametric waves and point-by-point mathematical control. CSS works fine for simpler effects like ripple buttons, but it lacks the flexibility of Canvas.
- Can I use
Math.sin()orMath.cos()interchangeably?- Yes. The main difference lies in their starting phase offset: sine starts at
0, whereas cosine starts at1. In practice, for visual animation effects, the results are virtually identical.
- Yes. The main difference lies in their starting phase offset: sine starts at
- Does this animation consume a lot of system resources?
- When properly optimized, no.
requestAnimationFrame()automatically pauses execution when the browser tab is hidden, keeping resource usage minimal.
- When properly optimized, no.
- Does this wave animation work on mobile devices?
- Yes, provided you scale the canvas resolution and adjust your loop iterations to fit the processing capabilities of mobile hardware.
Conclusion
Building a wave effect in JavaScript goes far beyond a simple visual trick. Combining HTML5 <canvas>, requestAnimationFrame(), and trigonometry gives you smooth, organic, fully customizable animations without relying on external libraries.
In my experience, moving beyond a single wave to overlapping multiple Math.cos() functions with distinct phase offsets is what truly elevated the final visual quality. From there, the possibilities are endless: dynamic colors, mouse reactivity, depth layers… the canvas wave animation serves as a incredibly versatile foundation.