Volumetric Light

ProInteractivitypowered by @vectorvesper/motion

A pointer-following light drawn with WebGL behind your content. Each element you mark blocks it, so soft shadows stream out behind it and light pours through the gaps, while text gets a matching text-shadow. Made for light and dark pages; without WebGL it falls back to CSS shadows.

Volumetric Light

Included with membership

Members install Volumetric Light with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.

When to use it

A hero or feature section built from separate pieces, such as cards, tiles and a headline, where you want the page itself to feel lit. The effect lives in the gaps: light pours between the pieces you mark and their shadows stream away from the pointer, so it needs a few solid, separate shapes to work with. Over one block of text or a full-bleed image there is little for it to cast.

  • Mark separate, solid pieces such as cards and tiles, and leave gaps between them; the gaps are where the light comes through.
  • Set ground to match the page behind: light for pale pages, dark for dark ones. The container paints no background, so the page colour stays yours.
  • Use restPosition to choose where the light waits before the visitor moves, such as a corner of the page or the gap between two cards.
  • Mark headlines too. Text elements (h1 to h6, p, span and a) don't block the light; they get a text-shadow that leans away from it.

Avoid:

  • Don't mark more than 24 blocking elements. Only the first 24 in page order block the light, and the rest cast nothing, with no warning.
  • Don't put the class on a wrapper. A wrapper blocks as one big box, so the gaps between its children go dark.
  • Don't expect see-through cards to let light through. Every marked element blocks as a solid rounded box, whatever its opacity.
  • Don't nest one inside another. Both lights claim the same marked elements and fight over their shadows.

Props

A light that follows the pointer, drawn with WebGL behind your content. Every element you mark blocks it, so soft shadows stream out behind each one and light pours through the gaps between them, while marked text gets a matching text-shadow. It has one look for light pages and one for dark pages, and falls back to CSS shadows where WebGL2 isn't available.

<VolumetricLight />

Wraps a region and lights it. Everything inside that matches `selector` takes part: text gets a text-shadow, and every other match blocks the light.

PropTypeDefaultDescription
childrenReact.ReactNode—The lit region.
selectorstring".shadow-cast"Which descendants take part. Text elements get a text-shadow; any other match blocks the light and catches a rim of it on the edge that faces it.
ground"light" | "dark""light"What the page behind is. Light warms what the light reaches and lays a cool shade where it is blocked; dark adds glowing light.
beamsbooleantrueDraws the light with WebGL behind the content, so shadows stream out behind each marked element and light pours through the gaps. Off, the plain glow and CSS shadows stand in.
beamColorstring"#FFB25E" on light, "#FFE3BC" on darkColour of the light, as hex or rgb(). Alpha is ignored.
shadeColorstring"#3B4A6B"Colour of the shade on a light ground, as hex or rgb().
beamStrengthnumber1Brightness of the light.
beamReachnumber1200How far the light carries, in pixels.
rimbooleantrueThe edge of each marked element that faces the light catches it.
restPosition[number, number] | string[0.5, 0.35]Where the light waits before the pointer arrives: fractions of the container's width and height, or a selector for an element inside it. Resting on an element, the light drifts only within that element's box.
idlebooleantrueUntil the visitor moves, the light drifts a little around its resting place. Always off with reduced motion.
dampingnumber0.08How far the light trails the pointer, from 0 to 0.95. Higher trails further.
intensitynumber0.08How far a text-shadow reaches for each pixel of distance from the light.
maxShadowLengthnumber100Longest a text-shadow gets, in pixels.
shadowColorstring"rgba(0, 0, 0, 0.45)"Colour of the text-shadows and of the CSS shadows. Its alpha sets how dark they get.
blurFactornumber1Multiplier on shadow blur. Lowered automatically on constrained devices.
showLightbooleantrueDraws the plain glow wherever the WebGL light can't be drawn.
lightSizenumber240Diameter of the plain glow, in pixels. Also sets how soft the edges of the shadows are.
lightColorstring"rgba(255, 240, 200, 0.55)"Colour at the centre of the plain glow.
lightBlurnumber40Blur on the plain glow, in pixels.
disableAdaptiveQualitybooleanfalseKeeps full shadow quality on constrained devices. By default the most constrained tier drops to a single shadow layer at 40% blur; set this and every device gets the three-layer shadow.
classNamestring""Applied to the container. Set the page colour here.
styleReact.CSSProperties—Inline styles merged onto the container.
With reduced motion the WebGL light is skipped and the plain glow and CSS shadows stand in. The light keeps up with the pointer instead of trailing it, and the idle drift stays off.
It runs on the shared frame loop rather than starting one of its own. The light is drawn at reduced resolution, lower still on constrained devices, which its soft edges hide, and only while the light or a marked element moves; a moment after everything settles it stops drawing. Off screen it measures and draws nothing. Changing the ground, colours, strength, reach or light size takes effect on the next frame without restarting the light, so it never blanks while a control is dragged.
On touch screens a tap moves the light and a sideways drag pulls it along, while an up-and-down drag still scrolls the page. The container sets touch-action to pan-y pinch-zoom for this; pass style={{ touchAction: "none" }} to let every drag steer the light, at the cost of scrolling over it.
A client component, so it can sit in a Server Component page as it is. Its children render on the server as normal and the light appears once it mounts in the browser.
The light needs WebGL2. Where it isn't available, and on devices too weak to draw it smoothly, the component switches to the plain glow and CSS shadows by itself, with no extra code.
Back to all components