You Might Also Like
Canvas Procedural Lightning — Free Midpoint Displacement Snippet
Canvas Procedural Lightning · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Canvas Procedural Lightning — Fractal Midpoint Displacement, Not Random Points

A convincing lightning bolt isn't a jagged line drawn from random points — it's a fractal, where the same jaggedness pattern repeats at every zoom level. This snippet builds bolts using recursive midpoint displacement, the classic technique also used to generate fractal terrain heightmaps and coastlines.
Displace the midpoint, then recurse on both halves
displace() takes a straight line from point A to B, finds its exact midpoint, nudges that midpoint sideways (perpendicular to the A-B direction) by a random amount, then recursively calls itself on the two new sub-segments — A-to-midpoint and midpoint-to-B — each with a smaller maximum displacement (maxOffset * 0.55). Because every recursive level operates on a shorter segment with a proportionally smaller displacement budget, the result has large kinks at the coarse level and progressively finer, subtler kinks nested inside them — exactly the self-similar roughness real lightning (and coastlines, and mountain ridgelines) actually has.
Perpendicular displacement, not free-form randomness
The midpoint is only ever nudged along the perpendicular to its segment (nx = -dy/len, ny = dx/len), never along the segment's own direction. That constraint is what keeps the bolt visually progressing from source to target — a lightning bolt with points randomly jittered in *every* direction would double back on itself and look like scribble, not electricity.
Branches spawn off the main bolt's segments, not independently
After the main bolt's point list is built, buildBolt() walks its interior segments and, with a fixed probability per segment, spawns a short secondary bolt starting from that segment's endpoint at a randomly offset angle — itself built with the same displace() function at a smaller depth and displacement range. Reusing the identical fractal-generation function for branches (just with different parameters) is what keeps branches visually consistent with the main bolt instead of looking like a different effect grafted on.
Two-layer stroke for the glow
Each bolt is stroked twice: once wide, low-opacity, and heavily blurred (shadowBlur: 22) for the outer glow, and once narrow, high-opacity, and lightly blurred for the bright core — the same "wide soft layer under a narrow bright layer" trick used for neon and energy effects generally.
A screen-flash pass sells the strike
On every new strike, a flash value spikes to 1 and decays exponentially; while positive, the frame-clearing fill uses a bright, near-white color instead of the usual translucent dark wash, producing a brief whole-canvas flash that reads as the strike illuminating the sky, just like real lightning.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why recursive midpoint displacement with a shrinking displacement budget per level produces self-similar, fractal-looking jaggedness, and why constraining displacement to the perpendicular direction is essential for the bolt to read as directed rather than scribbled. It's a great snippet to extend with an assistant — ask for a version with multiple simultaneous strikes across the sky, a version that strikes a specific DOM element's position (e.g. hitting a button on click for a "power surge" effect), or a slower-motion strike where the bolt animates growing from source to target instead of appearing instantly.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build an interactive "procedural lightning" effect in plain HTML, CSS, and JavaScript using only the Canvas 2D API and a hand-rolled recursive midpoint-displacement algorithm — no external library.
Requirements:
- A recursive function that takes a straight line segment (two endpoints) and a maximum displacement amount, computes the segment's exact midpoint, displaces that midpoint perpendicular to the segment's direction by a random amount within the displacement budget, then recursively calls itself on the two resulting sub-segments (start-to-midpoint and midpoint-to-end) with a smaller displacement budget (e.g. multiplied by ~0.55) and one less recursion depth. Collect the resulting displaced points into an ordered array forming the jagged bolt path.
- On click anywhere on the canvas, generate a new bolt from a point near the top of the canvas (randomized horizontally) down to the clicked point using that recursive displacement function, and add it to an active-bolts list (capping the list at a small number, dropping the oldest when exceeded).
- After generating the main bolt's point list, walk its interior segments and, with a fixed probability per segment, spawn a short secondary "branch" bolt starting at that segment's endpoint, at a randomly offset angle, generated using the SAME recursive displacement function at a smaller recursion depth and displacement range — branches should look visually consistent with the main bolt, not drawn with different logic.
- Render each bolt with a two-layer glow: first stroke it wide, low-opacity, and heavily blurred (via shadowBlur) for a soft outer glow, then stroke it again narrow, high-opacity, and lightly blurred for a bright core, with branches rendered similarly but thinner and more transparent than the main bolt.
- Fade each bolt's opacity out over roughly 15-25 frames after it strikes, removing it from the active list once fully faded, using a semi-transparent full-canvas fill each frame (rather than clearRect) so trails/afterimages linger briefly.
- On every new strike, trigger a brief whole-canvas "flash": for a few frames immediately after a strike, use a bright near-white fill (decaying exponentially back to the normal translucent dark fill) instead of the normal per-frame background fill, simulating the strike illuminating the surrounding sky.
- Expose a "Jaggedness" slider controlling the initial displacement budget passed into the recursive function. Handle window resize and scale for devicePixelRatio so bolts render crisply on high-DPI screens.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA demo bolt strikes automatically shortly after load.
- 2Click anywhere on the canvasA new jagged, branching bolt strikes from the sky to that point.
- 3Watch the flashThe whole canvas briefly brightens on each new strike.
- 4Watch bolts fadeEach bolt's opacity decays over roughly 20 frames.
- 5Adjust JaggednessHigher values produce wilder, more chaotic bolt paths.
- 6Strike repeatedlyUp to 4 bolts can be visible and fading at once.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Midpoint displacement is a fractal-generation technique: take a straight segment, displace its midpoint perpendicular to the segment by a random amount, then recurse on each half with a smaller displacement budget. Because each recursion level operates at a smaller scale with proportionally smaller displacement, the result is self-similar roughness — large kinks with progressively finer kinks nested inside them — which looks like real lightning, terrain, or coastlines, rather than the scribbly path a fully random set of points would produce.
Restricting displacement to the perpendicular direction (computed as the segment's direction vector rotated 90 degrees) guarantees the bolt keeps making net progress from its start point toward its target, since no displacement ever pushes a point backward along the segment's own direction. Removing that constraint would let points jitter freely and the bolt would double back on itself, reading as scribble rather than a directed strike.
Branches are generated by calling the exact same displace() function used for the main bolt, just starting from a point partway along the main bolt, at a randomly offset angle, with a smaller recursion depth and displacement range. Reusing the identical fractal-generation logic (rather than a separate, simpler branch-drawing routine) is what makes branches look like they belong to the same lightning, not a different visual effect layered on top.
Each new strike sets a flash variable to 1, which decays exponentially (multiplied by 0.85) every frame. While flash is above a small threshold, the per-frame background fill uses a bright, near-white color scaled by the current flash value instead of the usual translucent dark wash used for the trailing-fade effect, producing a brief brightening of the entire canvas that reads as the strike illuminating the surrounding sky.
Yes — strike() currently randomizes the bolt's starting x-position with a spread proportional to canvas width; replace that randomized startX with a fixed value (or a value derived from a visible "cloud" element's position) to always originate bolts from the same point, while leaving the recursive displace() and branch logic completely unchanged.