Fractal Glass
A premium WebGL refraction effect that splits the hero surface into shifting glass-like facets for brand launches and portfolio pages.

Overview
The hero can hold a refracted surface or a paragraph. Fractal Glass is for pages where the surface earns the paragraph.
Use it for premium heroes, AI/product launches, creative portfolios, and brand moments where the visual payoff justifies GPU work. It should make the site feel technically ambitious, not merely heavier.
The risk in production is budget. Keep meaningful content in HTML, cap DPR by device tier, reduce postprocessing on mobile, pause render loops offscreen, and ship a poster fallback that still feels intentional.
Install Command
npx hyperiux add fractal-glassUsage Code
import FractalGlass from "@/components/effects/fractal-glass";
const images = {
blob: "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/h-13.jpg",
};
export default function Home() {
return (
<>
<FractalGlass
imageSrc={images.blob}
stripesFrequency={40}
glassStrength={2.0}
glassSmoothness={0.014}
parallaxStrength={0.15}
distortionMultiplier={8.0}
edgePadding={0.12}
/>
</>
);
}
Component Code
// Built using Hyperiux Vault: https://vault.hyperiux.com
'use client';
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { createSuspendedRaf } from "./createSuspendedRaf";
const DEFAULT_IMAGE_SRC = "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/h-13.jpg";
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setPrefersReducedMotion(mediaQuery.matches);
update();
mediaQuery.addEventListener("change", update);
return () => mediaQuery.removeEventListener("change", update);
}, []);
return prefersReducedMotion;
}
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D uTexture;
uniform vec2 uResolution;
uniform vec2 uTextureSize;
uniform vec2 uMouse;
uniform float uParallaxStrength;
uniform float uDistortionMultiplier;
uniform float uGlassStrength;
uniform float uStripesFrequency;
uniform float uGlassSmoothness;
uniform float uEdgePadding;
varying vec2 vUv;
vec2 getCoverUV(vec2 uv, vec2 textureSize) {
if (textureSize.x < 1.0 || textureSize.y < 1.0) return uv;
vec2 s = uResolution / textureSize;
float scale = max(s.x, s.y);
vec2 scaledSize = textureSize * scale;
vec2 offset = (uResolution - scaledSize) * 0.5;
return (uv * uResolution - offset) / scaledSize;
}
float displacement(float x, float num_stripes, float strength) {
float modulus = 1.0 / num_stripes;
return mod(x, modulus) * strength;
}
float fractalGlass(float x) {
float stripeWidth = 1.0 / uStripesFrequency;
float sampleStep = uGlassSmoothness * stripeWidth;
float d = 0.0;
for (int i = -5; i <= 5; i++) {
d += displacement(x + float(i) * sampleStep, uStripesFrequency, uGlassStrength);
}
d = d / 11.0;
return x + d;
}
float smoothEdge(float x, float padding) {
float edge = padding;
if (x < edge) {
return smoothstep(0.0, edge, x);
} else if (x > 1.0 - edge) {
return smoothstep(1.0, 1.0 - edge, x);
}
return 1.0;
}
void main() {
vec2 uv = vUv;
float originalX = uv.x;
float edgeFactor = smoothEdge(originalX, uEdgePadding);
float distortedX = fractalGlass(originalX);
uv.x = mix(originalX, distortedX, edgeFactor);
float distortionFactor = uv.x - originalX;
float parallaxDirection = -sign(0.5 - uMouse.x);
vec2 parallaxOffset = vec2(
parallaxDirection * abs(uMouse.x - 0.5) * uParallaxStrength * (1.0 + abs(distortionFactor) * uDistortionMultiplier),
0.0
);
parallaxOffset *= edgeFactor;
uv += parallaxOffset;
vec2 coverUV = getCoverUV(uv, uTextureSize);
if (coverUV.x < 0.0 || coverUV.x > 1.0 || coverUV.y < 0.0 || coverUV.y > 1.0) {
coverUV = clamp(coverUV, 0.0, 1.0);
}
vec4 color = texture2D(uTexture, coverUV);
gl_FragColor = color;
}
`;
function resolveMediaSource(source) {
if (typeof source === "string")
return source;
if (source?.src)
return source.src;
return source;
}
function loadImageElement(source, fallbackSource = DEFAULT_IMAGE_SRC) {
return new Promise((resolve, reject) => {
const imageSource = resolveMediaSource(source) || fallbackSource;
if (!imageSource) {
reject(new Error("A valid image URL is required."));
return;
}
fetch(imageSource, {
mode: "cors",
credentials: "omit",
cache: "no-store",
})
.then((response) => {
if (!response.ok) {
throw new Error(`Image request failed with ${response.status}`);
}
return response.blob();
})
.then((blob) => {
const objectUrl = URL.createObjectURL(blob);
const image = new Image();
image.onload = () => resolve({ image, objectUrl });
image.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error(`Unable to decode glass strip image: ${imageSource}`));
};
image.src = objectUrl;
})
.catch((error) => {
if (imageSource !== fallbackSource) {
loadImageElement(fallbackSource, fallbackSource).then(resolve).catch(reject);
return;
}
reject(new Error(`Unable to load glass strip image: ${imageSource}. Current origin is ${window.location.origin}. Make sure the URL allows CORS for this site. ${error?.message || ""}`));
});
});
}
export default function FractalGlass({ imageSrc = DEFAULT_IMAGE_SRC, videoSrc = null, mediaType = "image", stripesFrequency = 40, glassStrength = 2.0, glassSmoothness = 0.014, parallaxStrength = 0.15, distortionMultiplier = 8.0, edgePadding = 0.12, }) {
const mountRef = useRef(null);
const videoRef = useRef(null); // keeps reference to video element for cleanup
const uniformsRef = useRef(null);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
const el = mountRef.current;
if (!el)
return;
const W = el.clientWidth;
const H = el.clientHeight;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(W, H);
renderer.domElement.setAttribute("aria-hidden", "true");
el.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
camera.position.z = 1;
const uniforms = {
uTexture: { value: new THREE.Texture() },
uResolution: { value: new THREE.Vector2(W, H) },
uTextureSize: { value: new THREE.Vector2(1, 1) },
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
uParallaxStrength: { value: parallaxStrength },
uDistortionMultiplier: { value: distortionMultiplier },
uGlassStrength: { value: glassStrength },
uStripesFrequency: { value: stripesFrequency },
uGlassSmoothness: { value: glassSmoothness },
uEdgePadding: { value: edgePadding },
};
uniformsRef.current = uniforms;
let videoEl = null;
let videoTexture = null;
let imageTexture = null;
let imageObjectUrl = null;
let isDisposed = false;
if (mediaType === "video" && videoSrc) {
// ── Video path ──────────────────────────────────────────────
videoEl = document.createElement("video");
const currentVideoEl = videoEl;
currentVideoEl.crossOrigin = "anonymous";
currentVideoEl.referrerPolicy = "no-referrer";
currentVideoEl.loop = true;
currentVideoEl.muted = true;
currentVideoEl.playsInline = true;
currentVideoEl.autoplay = true;
currentVideoEl.src = resolveMediaSource(videoSrc);
videoRef.current = currentVideoEl;
currentVideoEl.addEventListener("loadedmetadata", () => {
uniforms.uTextureSize.value.set(currentVideoEl.videoWidth, currentVideoEl.videoHeight);
});
videoEl.play().catch(() => {
// Autoplay blocked — still renders first frame when available
});
videoTexture = new THREE.VideoTexture(videoEl);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.wrapS = THREE.ClampToEdgeWrapping;
videoTexture.wrapT = THREE.ClampToEdgeWrapping;
uniforms.uTexture.value = videoTexture;
}
else {
// ── Image path ──────────────────────────────────────────────
loadImageElement(imageSrc).then(({ image, objectUrl }) => {
if (isDisposed)
return;
imageObjectUrl = objectUrl;
imageTexture = new THREE.Texture(image);
imageTexture.needsUpdate = true;
imageTexture.minFilter = THREE.LinearFilter;
imageTexture.magFilter = THREE.LinearFilter;
imageTexture.wrapS = THREE.ClampToEdgeWrapping;
imageTexture.wrapT = THREE.ClampToEdgeWrapping;
uniforms.uTexture.value = imageTexture;
uniforms.uTextureSize.value.set(image.naturalWidth || image.width || 1920, image.naturalHeight || image.height || 1080);
}).catch((error) => {
console.warn(`Unable to load glass strip texture: ${resolveMediaSource(imageSrc)}. Make sure the public URL allows CORS for this site.`, error);
});
}
const geo = new THREE.PlaneGeometry(2, 2);
const mat = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms });
scene.add(new THREE.Mesh(geo, mat));
const target = { x: 0.5, y: 0.5 };
const current = { x: 0.5, y: 0.5 };
const setTarget = (x, y) => {
target.x = x / window.innerWidth;
target.y = 1 - y / window.innerHeight;
};
const onMouse = (e) => setTarget(e.clientX, e.clientY);
const onTouch = (e) => setTarget(e.touches[0].clientX, e.touches[0].clientY);
window.addEventListener("mousemove", onMouse);
window.addEventListener("touchmove", onTouch, { passive: true });
const onResize = () => {
const w = el.clientWidth, h = el.clientHeight;
renderer.setSize(w, h);
uniforms.uResolution.value.set(w, h);
};
window.addEventListener("resize", onResize);
const loop = createSuspendedRaf({
root: el,
onFrame: () => {
current.x += (target.x - current.x) * 0.04;
current.y += (target.y - current.y) * 0.04;
uniforms.uMouse.value.set(current.x, current.y);
renderer.render(scene, camera);
},
});
loop.start();
return () => {
isDisposed = true;
uniformsRef.current = null;
loop.destroy();
window.removeEventListener("mousemove", onMouse);
window.removeEventListener("touchmove", onTouch);
window.removeEventListener("resize", onResize);
if (videoEl) {
videoEl.pause();
videoEl.src = "";
videoRef.current = null;
}
videoTexture?.dispose();
imageTexture?.dispose();
if (imageObjectUrl)
URL.revokeObjectURL(imageObjectUrl);
renderer.dispose();
mat.dispose();
geo.dispose();
if (el.contains(renderer.domElement))
el.removeChild(renderer.domElement);
};
}, [imageSrc, videoSrc, mediaType]);
useEffect(() => {
const uniforms = uniformsRef.current;
if (!uniforms)
return;
uniforms.uParallaxStrength.value = parallaxStrength;
uniforms.uDistortionMultiplier.value = distortionMultiplier;
uniforms.uGlassStrength.value = glassStrength;
uniforms.uStripesFrequency.value = stripesFrequency;
uniforms.uGlassSmoothness.value = glassSmoothness;
uniforms.uEdgePadding.value = edgePadding;
}, [stripesFrequency, glassStrength, glassSmoothness, parallaxStrength, distortionMultiplier, edgePadding]);
return (<div ref={mountRef} style={{
position: "fixed",
inset: 0,
width: "100vw",
height: "100vh",
overflow: "hidden",
background: "#000",
}}>
{/* Mobile message */}
<div className="
hidden
max-[1025px]:flex
fixed
bottom-6
left-1/2
-translate-x-1/2
z-50
px-4
py-2
rounded-full
bg-white/10
backdrop-blur-md
text-white
text-center
text-sm
leading-tight
pointer-events-none
max-md:px-[7vw] max-md:py-[4vw]
">
Works best on desktop
</div>
{prefersReducedMotion && (<div aria-live="polite" className="pointer-events-none fixed bottom-4 right-4 z-40 w-fit max-w-65 rounded-md border border-white/15 bg-white/5 p-3 text-center backdrop-blur-sm max-md:hidden">
<h2 className="text-sm leading-none text-white">
The glass keeps shifting.
</h2>
<p className="mt-2 text-xs leading-5 text-white/65">
Fractal Glass distorts the image based on cursor and touch
position in real time. Since the distortion is driven entirely
by motion, reduced motion can't be applied here.
</p>
</div>)}
</div>);
}
const DEFAULT_ROOT_MARGIN = "256px";
function resolveElement(root) {
if (!root)
return null;
if (typeof root === "function")
return root() ?? null;
if (typeof root === "object" && "current" in root)
return root.current ?? null;
return root;
}
function createVisibilityGate({ root = null, rootMargin = DEFAULT_ROOT_MARGIN, threshold = 0, observeTab = true, observeOffscreen = true, onChange, } = {}) {
let tabVisible = typeof document === "undefined" ? true : !document.hidden;
// Match border-beam: assume onscreen until the observer reports otherwise.
let onscreen = true;
let destroyed = false;
let observer = null;
const isActive = () => {
if (destroyed)
return false;
if (observeTab && !tabVisible)
return false;
if (observeOffscreen && resolveElement(root) && !onscreen)
return false;
return true;
};
let lastActive = isActive();
const emit = () => {
if (destroyed)
return;
const next = isActive();
if (next === lastActive)
return;
lastActive = next;
onChange?.(next);
};
const onVisibilityChange = () => {
tabVisible = !document.hidden;
emit();
};
if (observeTab && typeof document !== "undefined") {
document.addEventListener("visibilitychange", onVisibilityChange);
}
const bindObserver = () => {
if (!observeOffscreen || typeof IntersectionObserver === "undefined") {
return;
}
const el = resolveElement(root);
if (!el)
return;
observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
onscreen = entry.isIntersecting;
}
emit();
}, { rootMargin, threshold });
observer.observe(el);
};
bindObserver();
return {
/** Whether the animation should currently run. */
get isActive() {
return isActive();
},
/**
* Re-bind IntersectionObserver after the root element mounts late
* (e.g. ref not ready on first call). Safe to call multiple times.
*/
observe(nextRoot) {
if (destroyed)
return;
if (nextRoot != null)
root = nextRoot;
if (observer) {
observer.disconnect();
observer = null;
}
onscreen = true;
bindObserver();
emit();
},
destroy() {
if (destroyed)
return;
destroyed = true;
if (observeTab && typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibilityChange);
}
if (observer) {
observer.disconnect();
observer = null;
}
},
};
}
/**
* Owns a requestAnimationFrame loop that auto-pauses when the tab is hidden
* or the root element is offscreen.
*/
function createSuspendedRaf({ onFrame, root = null, rootMargin = DEFAULT_ROOT_MARGIN, threshold = 0, observeTab = true, observeOffscreen = true, }) {
if (typeof onFrame !== "function") {
throw new TypeError("createSuspendedRaf: onFrame is required");
}
let rafId = null;
let running = false;
let destroyed = false;
const stopRaf = () => {
if (rafId != null) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
const tick = (time) => {
rafId = null;
if (destroyed || !running || !gate.isActive)
return;
onFrame(time);
if (!destroyed && running && gate.isActive) {
rafId = requestAnimationFrame(tick);
}
};
const sync = () => {
if (destroyed)
return;
if (running && gate.isActive) {
if (rafId == null) {
rafId = requestAnimationFrame(tick);
}
}
else {
stopRaf();
}
};
const gate = createVisibilityGate({
root,
rootMargin,
threshold,
observeTab,
observeOffscreen,
onChange: sync,
});
return {
/** Start (or resume) the loop when visibility allows. */
start() {
if (destroyed)
return;
running = true;
sync();
},
/** Stop requesting frames (visibility listeners stay attached until destroy). */
stop() {
running = false;
stopRaf();
},
/** Whether the caller has started the loop (may still be paused by visibility). */
get isRunning() {
return running;
},
/** Whether a frame is currently allowed to schedule. */
get isActive() {
return gate.isActive;
},
/** Re-attach offscreen observer to a (new) root element. */
observe(nextRoot) {
gate.observe(nextRoot);
sync();
},
/** Tear down listeners and cancel any pending frame. */
destroy() {
if (destroyed)
return;
destroyed = true;
running = false;
stopRaf();
gate.destroy();
},
};
}
export { createSuspendedRaf, createVisibilityGate, DEFAULT_ROOT_MARGIN, };
Example Production Use Case
Use this as WebGL production guidance. Verify the shipped source, rendering stack, dependency list, shader assets, resource disposal, pause/offscreen behavior, DPR strategy, poster fallback, and reduced-motion state before relying on exact props, defaults, imports, or installation steps.
Best Used For
- Premium brand heroes where refraction and facets can prove craft without burying the message.
- WebGL surfaces that have a poster fallback, mobile simplification, and clear HTML content.
- Fractal Glass gives the hero a proof-of-craft surface while preserving HTML copy and fallback design.
Not For
Not for low-power-first pages, dense content, dashboards, checkout, or routes where performance is the main conversion lever.
Performance Budget
Cap DPR at 1.0 on touch/mobile and up to 1.5 on mid-range desktop. Pause Fractal Glass offscreen and in hidden tabs, reduce postprocessing before shipping, and keep texture sizes controlled.
Accessibility and Mobile
Keep meaningful content in HTML outside the canvas. On mobile, reduce shader quality and switch to a poster if the scene cannot hold frame rate.
Common Mistakes
- Making Fractal Glass the only place the message exists.
- Shipping the highest-cost visual tier on mobile without simplification or fallback.
- Forgetting poster fallback and render-loop pause.
Changelog
v1.1.0
Jul 22, 2026v1.0.1
Jul 15, 2026Props
| Prop | Type | Default | Description |
|---|---|---|---|
stripesFrequency | number | 40 | Controls how many refractive glass strips are rendered. |
glassStrength | number | 2 | Controls the intensity of the strip refraction. |
glassSmoothness | number | 0.014 | Controls how smoothly neighboring refraction samples blend. |
parallaxStrength | number | 0.15 | Controls pointer-reactive horizontal parallax. |
distortionMultiplier | number | 8 | Amplifies parallax around distorted stripe regions. |
edgePadding | number | 0.12 | Softens distortion near the media edges. |
Frequently Asked Questions
What GPU budget should Fractal Glass use on mobile and desktop?
Refraction shaders are demanding, so cap DPR at 1.0 on mobile and up to 1.5 on desktop and reduce shader complexity or sample count on the lower tier. Use the quality lever to scale the refraction down rather than disabling it abruptly. Profile on real mobile GPUs, where heavy fragment shaders struggle most.
What poster fallback should replace Fractal Glass when WebGL fails?
Capture a still of the refracted surface so the fallback keeps the premium, faceted look without the live shader. The hero copy is HTML and reads regardless. Make that still good enough to stand on its own.
How should Fractal Glass pause when offscreen or in a hidden tab?
Run the shader on demand and stop it when the canvas is out of view or the tab is hidden, resuming on visibility. A live refraction pass is one of the most expensive things to leave running unseen. Visibility-gating it protects frame rate and battery.
Which content should stay outside the Fractal Glass canvas?
The paragraph the glass is meant to frame the headline, claim, and CTA stays in HTML in front of the surface, never inside the shader. The refraction earns attention for the copy; it should never replace it. Keep all actions as real DOM elements.
When should I choose a static image instead of Fractal Glass?
Choose a static treatment when the refraction is decorative, when performance or mobile reach is the priority, or when text legibility over the surface is at risk. If a still conveys the same crafted feel, prefer it. Reserve the live shader for pages where the surface genuinely earns the paragraph.
Can I adapt Fractal Glass to my brand and keep the hero readable?
Yes, adaptation work tunes the refraction, palette, and contrast so copy stays legible, with motion rules, a performance budget, fallback design, source handoff, and implementation notes. The component ships into and is owned by your codebase.
Request a Custom WebGL Animation
Need a custom effect? Tell us what to create.


