iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Canvas 2D has no bloom function. You can still fake a convincing glow with three steps you already have in the API: draw the bright elements to a separate canvas, blur that canvas with the filter property, and add the blurred result back onto the scene with a lightening composite. The core is a short function, and the one thing to check before you rely on it is whether your target browsers support filter.
What Canvas 2D does and does not provide
Bloom in real engines is usually a post-processing pass: the frame is thresholded so only the brightest pixels remain, those pixels are blurred over several passes, and the blur is added back to the frame. The Canvas 2D context does not expose any of that as a named feature. What it does expose is the pieces you need to build an approximation:
- Blur. The
filterproperty acceptsblur(), which MDN describes as a Gaussian blur. Larger values produce a wider, softer result. (MDN: CanvasRenderingContext2D filter property) - Lightening composites.
globalCompositeOperationincludeslighterandscreen, both of which brighten the destination where the source overlaps it. (MDN: CanvasRenderingContext2D globalCompositeOperation property) - Shadows.
shadowBlurandshadowColorcan draw a soft halo around any shape you draw. This is a per-shape effect, not a full-frame pass.
The approach below uses the first two. The shadow route is covered later as a shorter alternative.
The eight-line bloom
Create two offscreen canvases once, at the same size as your game canvas. One holds only the bright elements. The other holds the blurred copy. Recreate both if the game canvas is resized.
#1 Best Overall
Step 1: set up the offscreen canvases
const bright = document.createElement('canvas');
const blurred = document.createElement('canvas');
bright.width = blurred.width = canvas.width;
bright.height = blurred.height = canvas.height;
const b = bright.getContext('2d');
const o = blurred.getContext('2d');
Step 2: the per-frame function
The function body is eight statements. It draws the bright pass, blurs it, and composites the result onto the frame you pass in.
function bloom(ctx, drawBright) {
b.clearRect(0, 0, bright.width, bright.height);
drawBright(b);
o.clearRect(0, 0, blurred.width, blurred.height);
o.filter = 'blur(12px)';
o.drawImage(bright, 0, 0);
ctx.globalCompositeOperation = 'lighter';
ctx.drawImage(blurred, 0, 0);
ctx.globalCompositeOperation = 'source-over';
}
Step 3: call it after the normal scene
Draw the frame as usual, then call bloom() with a callback that redraws only the elements that should glow.
Rank #2
drawScene(ctx);
bloom(ctx, (g) => drawStars(g));
The callback receives the offscreen context, so any function that draws with a context parameter can be reused. Keep the bright-pass callback to the sprites, particles, or pickups that should glow. Everything else stays out of it, and that choice is what makes the effect look like bloom rather than a general haze. The 12px blur value is a starting point; raise it for a wider glow and lower it for a tighter one.
Blend mode: lighter or screen
Both modes brighten the destination, but they combine colour differently. lighter adds the source and destination colour values where shapes overlap, so bright areas clip quickly toward white. screen lightens without that hard addition, which usually keeps highlights from blowing out. The example uses lighter because it is the most direct additive glow. Switch the composite string to 'screen' if overlapping glows look washed out.
Compatibility and fallback
The filter property is not universal. MDN labels it as limited availability and says it is not Baseline because it does not work in some of the most widely used browsers. Check support in the browsers your players actually use before shipping the blur route.
A simple feature check is 'filter' in ctx. This tells you whether the property exists in the browser, not whether every blur renders identically, so verify the output in your target browsers. If the check fails, use the shadow halo instead:
Rank #4
if ('filter' in ctx) {
bloom(ctx, drawBrightSprites);
} else {
ctx.shadowColor = 'rgba(255, 200, 80, 0.9)';
ctx.shadowBlur = 24;
drawBrightSprites(ctx);
ctx.shadowBlur = 0;
}
The shadow branch draws the bright sprites directly on the main context, so it is a per-shape halo that runs in the same pass as the rest of the frame. It is not identical to the blurred composite, and the look will differ.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Choosing between the blur route and the shadow halo
| Factor | Blur plus lightening composite | Shadow halo |
|---|---|---|
| Browser support | filter is limited availability per MDN; not Baseline |
Shadow properties are part of the Canvas 2D API reference; still confirm in your target browsers |
| Visual result | One blurred glow layer over the whole frame | A halo around each shape you draw; looks different from a composited pass |
| Setup | Two offscreen canvases and resize handling | Three property assignments around each draw call |
| Performance | Not stated by the cited Canvas references; measure in your game | Not stated by the cited Canvas references; measure in your game |
What this approach does not do
- It has no threshold. You decide what is bright by choosing what goes into the bright pass.
- It does not model high dynamic range. Colour values are the ordinary 8-bit values the canvas stores.
- It is not a standard bloom effect. The output is a blurred additive layer, so it may look softer or brighter than a multi-pass post-process.
- No frame-rate figure applies. The Canvas references do not give a cost for a blur on an offscreen canvas, and the cost depends on canvas size, the number of frames, and the device.
When a library route is the wrong fit
If you already use PixiJS, its blur filter is an alternative, but it is a different renderer. The PixiJS 7.x container guide describes container filters, including blur, as advanced features that are WebGL-only, and warns that filters can slow performance and increase memory use when applied often. That conflicts with a no-WebGL requirement, so for this title it marks a boundary rather than a solution. The statement applies to the cited 7.x guide, not to every PixiJS version or renderer. (PixiJS 7.x Guides: Containers)
Best Value
For a Canvas 2D game with a small number of glowing elements, the two-canvas function and the shadow fallback cover the common cases without adding a rendering library.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

