How I Made the Sparkle Background
You may have noticed the twinkling stars behind the text on this website. If you haven't, scroll back up and look. I'll wait.
It's about 40 lines of vanilla JavaScript on an HTML5 canvas. No libraries. No WebGL. No shader language. Just arc() and Math.sin(). The most boring possible way to make something sparkle.
The setup
The trick is a <canvas> element that covers the entire viewport, fixed in position with pointer-events: none so it doesn't interfere with anything. It sits at z-index: 0 while the actual content floats above it.
<canvas id="sparkles"></canvas>
<style>
#sparkles {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
}
</style>
That's the entire HTML and CSS portion. Everything else is JavaScript.
The stars
On load, I generate 80 star objects with random properties:
- Position -- random x/y across the viewport
- Size -- between 0.5 and 2 pixels (tiny)
- Opacity -- a base brightness, so some stars are naturally dimmer
- Speed and phase -- controls the twinkle rate and offset
function createStars(count) {
stars = [];
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * w,
y: Math.random() * h,
size: Math.random() * 1.5 + 0.5,
opacity: Math.random(),
speed: Math.random() * 0.005 + 0.002,
phase: Math.random() * Math.PI * 2,
});
}
}
The stars are regenerated on window resize so they always fill the viewport. No stars floating in the void beyond the screen edge.
The twinkle
The animation loop is where the magic happens, and by "magic" I mean a sine wave:
function draw(t) {
ctx.clearRect(0, 0, w, h);
for (const s of stars) {
const opacity = (Math.sin(t * s.speed + s.phase) + 1) / 2 * 0.6 + 0.1;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity * s.opacity})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
}
Each star's opacity oscillates between 0.1 and 0.7 on a sine curve. Because each star has a different speed and phase offset, they all twinkle independently. The result looks organic even though it's entirely deterministic (well, after the initial random seed).
The t parameter comes from requestAnimationFrame, which passes a high-resolution timestamp in milliseconds. Multiply by a small speed factor and you get a smooth, slow oscillation.
The brand sparkle
On this site, about 3% of the time a star is drawn, it renders in the brand red instead of white:
const isBrand = Math.random() > 0.97;
ctx.fillStyle = isBrand
? `rgba(138, 7, 7, ${opacity})`
: `rgba(255, 255, 255, ${opacity * s.opacity})`;
This means stars don't have a fixed color -- any star might flash red on any given frame. It creates a subtle, almost subliminal effect. You notice the color without quite being able to pin it down.
Performance
Drawing 80 circles per frame is nothing for a modern GPU. Canvas 2D operations are hardware-accelerated on every browser that matters, and requestAnimationFrame automatically syncs to the display refresh rate and pauses when the tab is in the background.
There's no performance optimization here because none is needed. 80 arc() calls is a rounding error in your GPU's day.
The full thing
Here's the complete script, ready to drop into any page:
(function() {
const canvas = document.getElementById('sparkles');
const ctx = canvas.getContext('2d');
let w, h, stars = [];
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
}
function createStars(count) {
stars = [];
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * w,
y: Math.random() * h,
size: Math.random() * 1.5 + 0.5,
opacity: Math.random(),
speed: Math.random() * 0.005 + 0.002,
phase: Math.random() * Math.PI * 2,
});
}
}
function draw(t) {
ctx.clearRect(0, 0, w, h);
for (const s of stars) {
const opacity = (Math.sin(t * s.speed + s.phase) + 1) / 2 * 0.6 + 0.1;
const isBrand = Math.random() > 0.97;
ctx.fillStyle = isBrand
? `rgba(138, 7, 7, ${opacity})`
: `rgba(255, 255, 255, ${opacity * s.opacity})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
}
resize();
createStars(80);
window.addEventListener('resize', () => { resize(); createStars(80); });
requestAnimationFrame(draw);
})();
Swap the color values, change the star count, adjust the speed range. It's 40 lines. You can read every one of them. That's the point.
The best visual effects are the ones simple enough to explain in a blog post and boring enough to never break in production.
Markus and I build software together. If you want to work with us, get in touch.