Shaders: small WebGL effects you can copy
Nine background and motion effects for a web page, each running live below. Change the controls, then copy the code under the effect. Every effect is one small module with no dependency.
Written by us
We wrote every effect from scratch from standard graphics maths, in the Last Table colours. No code comes from a shader gallery or library, and no outside image is used.
Star field
A slow star field for a hero: three depths of stars drift sideways and twinkle over a low teal haze.
<div id="starfield-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import starfield from "https://tools.openkrill.app/shaders/effects/starfield.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("starfield-target"), starfield);
</script>Completion burst
One brass burst when a task completes: a short flash, a ring, rays and sparks that fall a little. It plays once, in 1.4 seconds.
Page content sits under this effect.
<div id="burst-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import burst from "https://tools.openkrill.app/shaders/effects/burst.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
const fx = mount(document.getElementById("burst-target"), burst);
fx.play(); // call fx.play() again each time it should fire
</script>Nebula
A slow nebula for a page background: soft clouds of teal gas over midnight, lit in a few places by brass.
<div id="nebula-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import nebula from "https://tools.openkrill.app/shaders/effects/nebula.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("nebula-target"), nebula);
</script>Glass shimmer
A glass shimmer over a card: a soft band of light crosses the card, then rests, and the top edge keeps a thin sheen.
Sample card
The shimmer passes over this card and its text.
<article id="shimmer-target">Your card</article>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import shimmer from "https://tools.openkrill.app/shaders/effects/shimmer.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("shimmer-target"), shimmer);
</script>Film grain
A film grain layer over any block: light and dark specks that change a few times a second, like film.
Page content sits under this effect.
<div id="grain-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import grain from "https://tools.openkrill.app/shaders/effects/grain.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("grain-target"), grain);
</script>Difference ripple
A ripple that points at one difference, as in Mirror Twin: a few teal rings spread from one point and fade. It plays once, in 1.8 seconds.
Page content sits under this effect.
<div id="ripple-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import ripple from "https://tools.openkrill.app/shaders/effects/ripple.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
const fx = mount(document.getElementById("ripple-target"), ripple);
fx.play(); // call fx.play() again each time it should fire
</script>Status pulse
A pulse for a status chip: a dot with a soft glow, and one ring that leaves the dot each period.
<span id="pulse-target" style="display: inline-block; width: 24px; height: 24px"></span>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import pulse from "https://tools.openkrill.app/shaders/effects/pulse.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("pulse-target"), pulse);
</script>Brass facets
Slow facets for a section background: midnight cells drift like cut glass, with thin brass seams between them.
<div id="facets-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import facets from "https://tools.openkrill.app/shaders/effects/facets.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("facets-target"), facets);
</script>Brass glow
A soft brass glow behind a title or a main button: the halo breathes slowly and its edge wavers a little.
<div id="glow-target" style="min-height: 60vh"></div>
<script type="module">
import { mount } from "https://tools.openkrill.app/shaders/runtime.js";
import glow from "https://tools.openkrill.app/shaders/effects/glow.js";
// To use an element you already have, pass it instead, such as document.querySelector(".hero").
// The element needs a size. The effect fills it and keeps its content on top, so no extra CSS is needed.
mount(document.getElementById("glow-target"), glow);
</script>Put an effect on your page
- Add an element with a size, such as a hero block, a card or a 24 px dot. An element you already have works too.
- Import the runtime and one effect as ES modules. Both are plain files on this site, so there is no build step and no package to install.
- Call
mount(element, effect, values). A loop starts at once. A one-shot effect (burst, ripple) waits forplay().
mount returns controls: set(values) changes parameters while it runs, play(), stop(), seek(seconds) and destroy(). A back effect sits under the element's content and a front effect over it; neither takes clicks, and no extra CSS is needed. A colour value can be a CSS variable such as "var(--lt-glow-fill)", read from the element when it mounts. When the element has no size, the browser console says so.
Phones, reduced motion and old browsers
- Without WebGL2 the element gets the effect's CSS background instead, so the page never shows an empty box.
- With reduced motion turned on, the effect draws one still frame and does not move.
- An effect stops drawing when it is off screen or the tab is hidden, and goes on where it stopped.
- On a phone it draws at most 30 frames a second at 1.5 times the pixel ratio. When frames keep coming late it draws fewer pixels, down to half.
Exact frames for video
Each frame is a pure function of time: the same second, size and values give the same picture, and no clock is read. renderFrame(effect, { t, width, height, params }) returns the RGBA bytes of one frame and renderPng returns a PNG. Video Forge can draw frame 412 of a film without drawing frames 0 to 411. The hash behind the noise uses whole numbers, not sine, so the same seed gives the same stars on every GPU.
File sizes
The shared files (runtime, maths and palette) are 15.1 KB, 6.4 KB gzipped, and are loaded once for any number of effects.
| File under /shaders/ | Bytes | Gzipped bytes |
|---|---|---|
| glsl.js | 3136 | 1613 |
| index.js | 814 | 338 |
| palette.js | 560 | 373 |
| runtime.js | 11779 | 4580 |
| snippet.js | 1639 | 864 |
| effects/burst.js | 2122 | 1119 |
| effects/facets.js | 1544 | 797 |
| effects/glow.js | 1714 | 860 |
| effects/grain.js | 1477 | 843 |
| effects/nebula.js | 1792 | 901 |
| effects/pulse.js | 1350 | 744 |
| effects/ripple.js | 1589 | 836 |
| effects/shimmer.js | 1612 | 856 |
| effects/starfield.js | 2488 | 1181 |
Originality
The effects are built from textbook techniques: an integer hash (PCG), value noise, simplex noise, fractal noise sums, domain warping, voronoi cells, gaussian rings, film grain and soft glow. We read about techniques, then wrote our own code without copying or rewording code from a shader gallery, a shader library or another renderer. You may copy this code into your own pages.
Each module starts with a header that names the effect, its parameters, its author and its size in bytes. As of 2026-10-07.