Glass Curtain

ProInteractivityWebGLpowered by @vectorvesper/motion

A refractive glass curtain overlay that distorts whatever sits behind it. Extends the native div props, so class names, layout and event handlers pass straight through to the underlying element.

Glass Curtain

Included with membership

Members install Glass Curtain 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 a feature panel where you want the image present but not fully legible — the fluted glass reads as a surface in front of the picture rather than a filter on it. It works best over one strong image with text sitting on top; over a detailed photograph the ridges compete with the content.

  • Give the wrapper an explicit height — it fills its container, and an unsized container renders nothing.
  • Put your caption or heading in children; it renders above the glass and stays sharp.
  • Raise ridges for a finer, more optical surface and lower it for wide bands — this is the prop that changes the character most.
  • Use progress when you want the curtain driven by your own scroll or state rather than the pointer.

Avoid:

  • Do not put body copy behind the glass. Ridged refraction makes small text unreadable by design.
  • Do not set both src and videoSrc expecting a fallback chain — pick one source for the surface.
  • Do not serve the media from another domain without CORS headers. The surface has to sample the pixels, and without them the canvas stays blank behind the plain image.

Props

An image or video seen through ribbed glass. The ribs bend what is behind them and shift as the cursor moves across, like looking through a shower screen. Can be one panel or a pair that parts down the middle.

<GlassCurtain />

The panel. Accepts any div attribute alongside its own props, so className, style, id and event handlers all pass through.

PropTypeDefaultDescription
refReact.Ref<HTMLDivElement>—Forwarded to the outer element, so you can measure the panel or drive it from your own scroll code.
srcstring—Image behind the glass. Use this or videoSrc.
videoSrcstring—Video behind the glass. Takes precedence if both are set.
altstring""Alternative text for the image. Leave empty only if the panel is decorative.
mode"single" | "split""single"One sheet of glass, or two that meet in the middle and draw apart. Split reads as a curtain; single as a window.
side"left" | "right" | "top" | "bottom""right"Which edge a single panel is anchored to.
orientation"horizontal" | "vertical""horizontal"Which way the ribs run.
ridgesnumber16How many ribs across the panel. More gives a finer, busier texture.
strengthnumber1.0How hard each rib bends the image. 0 leaves the picture undistorted.
warpIntensitynumber1.0Distortion layered on top of the ribs, as the glass flexes. Independent of `strength`, which is the ribbing itself.
idlenumber0.4How much the glass drifts on its own with no pointer near it. 0 holds it still.
pointerDampnumber1.8How quickly the glass follows the cursor. Higher tracks tightly; lower lags behind it.
hoverDampnumber2.0The same, for the panel's reaction to being hovered at all.
progressnumberdriven by the pointerTakes over the open/closed position, 0–1, so you can drive the curtain from a scroll or timeline instead of the cursor.
childrenReact.ReactNode—Rendered over the glass, for captions or overlay content.
crossOrigin"anonymous" | "use-credentials""anonymous" for media on another domainHow the media is fetched. The effect has to read the pixels back, which the browser only permits on a cross-origin file that was requested this way — so anything served from another domain gets it by default. Set it yourself only when the host needs credentials.
imagePropsReact.ComponentPropsWithoutRef<"img">—Passed to the underlying image — loading, sizes, decoding and the like.
videoPropsReact.ComponentPropsWithoutRef<"video">—Passed to the underlying video — muted, loop, playsInline and the like.
classNamestring""Applied to the wrapper.
dprnumberthe screen's, capped at 2Pixel ratio the surface is drawn at. Lower it to trade sharpness for frame rate on a large curtain.
disableAdaptiveQualitybooleanfalseDraws at full resolution on every device. By default the surface drops to 1.0 on a mid tier and 0.75 on the most constrained one; set this and it stays wherever dpr puts it.
mount"auto" | "eager""eager"When to build the glass. The default builds it on mount. Set "auto" to wait until the panel is near the viewport and the page has frame headroom, which defers compiling the surface and uploading the source image. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference. A scene running its own loop would be invisible to the scheduler, so it could not be set aside under load, and it would slow every other animation on the page rather than only itself.
The idle drift and the pointer response both keep running under reduced motion. Set idle={0} and pass a fixed progress when the preference is set for a still panel that still shows the ribbing.
The distortion is one GPU pass over the source, so ribs are close to free. The cost is the source itself — a large video behind the glass is the expensive part.
Client component. It opens a GPU drawing context and reads the pointer, so it has to run in the browser.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. The pointer position it follows is read once per frame and shared with every other component that wants it, rather than each attaching its own listener. If the browser takes the drawing context away — which it does under memory pressure, and more often on a page with several of these — the component reports it and rebuilds once the context comes back, instead of leaving a permanently black rectangle.
Back to all components