shadimage
gammaui
uiaurora-glasssource: ok · hit

Aurora Glass

A shimmering glass tile background with ripple layers, bevel shading, and chromatic spread.

npx shadcn add "https://www.gammaui.com/r/aurora-glass.json"
free

Visual style

glass-blur

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
react

Files1 · source cached 10/9/2026, 1:28:14 PM

registry/gammaui/aurora-glass.tsx
"use client"

import { useEffect, useRef } from "react"
import type { ReactNode } from "react"

/**
 * AuroraGlass
 * A shimmering background of colorful glass tiles, rendered with raw WebGL2.
 *
 * A handful of broad directional ripple layers sweep across the whole tile
 * lattice in one continuous field (sampled per-fragment in grid-space), so
 * the highlight band flows smoothly from tile to tile rather than each tile
 * animating independently. Each tile's rounded-rect bevel normal warps the
 * sample point near its edges, giving a refraction-like bend as the band
 * crosses every tile.
 *
 * Props
 * ----------------------------------------------------------------------
 * width            string | number   "100%"     Container width
 * height           string | number   "100%"     Container height
 * className        string            ""         Additional CSS classes
 * children         ReactNode         undefined  Content rendered above the effect
 * speed            number            1          Animation speed multiplier (0-3)
 * tileDensity      number            4          Tiles across the surface (1-16)
 * rippleLayers     number            6          Stacked ripple layers (1-8)
 * warpStrength     number            0.33       Per-tile inverse-distance warp (0-0.6)
 * bandSharpness    number            3          Highlight peak sharpness (0.5-10)
 * chromaticSpread  number            0          Per-channel separation (0-1)
 * colorA           string            "#1E00FF"  Gradient stop (deep color)
 * colorB           string            "#D765E6"  Gradient stop (bright color)
 * backgroundColor  string            "#FFFFFF"  Fill where the field is dark
 * opacity          number            1          Master alpha (0-1)
 * dpr              number            1.5        Max device pixel ratio (1-3)
 *
 * Usage
 *   <AuroraGlass colorA="#1E00FF" colorB="#D765E6" tileDensity={6}>
 *     <h1>Your content, rendered above the effect</h1>
 *   </AuroraGlass>
 */

const MAX_RIPPLE_LAYERS = 8

const VERTEX_SRC = `#version 300 es
in vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}
`

const FRAGMENT_SRC = `#version 300 es
precision highp float;

uniform vec2 u_resolution;
uniform float u_time;
uniform float u_speed;
uniform float u_tileDensity;
uniform int u_rippleLayers;
uniform float u_warpStrength;
uniform float u_bandSharpness;
uniform float u_chromaticSpread;
uniform vec3 u_colorA;
uniform vec3 u_colorB;
uniform vec3 u_backgroundColor;
uniform float u_opacity;

out vec4 fragColor;

float hash21(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

vec2 hash22(vec2 p) {
  float n = hash21(p);
  float n2 = hash21(p + 17.13);
  return vec2(n, n2);
}

float sdRoundRect(vec2 p, vec2 halfSize, float r) {
  vec2 q = abs(p) - halfSize + r;
  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}

// Height field for the tile surface: flat in the middle, with a wide
// rounded bevel that rises toward the border -- like a slightly domed pane
// of glass set in a frame. d is the rounded-rect SDF (negative inside, 0 at
// the border). Near the border height is highest; deep inside it flattens.
float tileHeight(float d, float r) {
  float bevelWidth = r * 4.0;
  float distFromEdge = clamp(-d / bevelWidth, 0.0, 1.0);
  float bevel = 1.0 - distFromEdge;
  return bevel * bevel * (3.0 - 2.0 * bevel);
}

// One directional ripple band. Layer 0-2 are broad, slow, coherent sweeps
// (the main visible "ribbons"); layers beyond that add progressively finer,
// dimmer secondary detail so increasing rippleLayers enriches the texture
// without diluting the primary bands.
float rippleLayer(vec2 guv, float t, int i) {
  float fi = float(i);
  float dirAngle = 0.55 + fi * 0.7;
  vec2 dir = vec2(cos(dirAngle), sin(dirAngle));
  vec2 perp = vec2(-dir.y, dir.x);

  float freqBase = 0.16 + fract(fi * 0.31) * 0.1;
  float freq = freqBase * (1.0 + fi * 0.22);
  float speedVar = 0.35 + fract(fi * 0.53) * 0.35;
  float phase = fi * 2.7;
  float bow = 1.6 + fract(fi * 0.19) * 1.4;

  float along = dot(guv, dir);
  float across = dot(guv, perp);

  float bend = sin(along * freq + t * speedVar + phase) * bow
             + cos(t * speedVar * 0.5 + phase * 1.3) * 1.2;
  float d = abs(across - bend);

  float sharpness = 6.0 + fi * 1.5;
  float core = exp(-d * d * sharpness);
  float halo = exp(-d * d * 1.1) * 0.15;

  float weight = 1.0 / (1.0 + fi * 0.55);

  return (core + halo) * weight;
}

// Sum of N ripple layers sampled at this grid-space position. The field is
// continuous across the whole grid (not per-tile), so highlights flow
// smoothly from one tile into its neighbor.
float lightField(vec2 guv, float t) {
  float field = 0.0;
  for (int i = 0; i < ${MAX_RIPPLE_LAYERS}; i++) {
    if (i >= u_rippleLayers) break;
    field += rippleLayer(guv, t, i);
  }
  return field;
}

void main() {
  vec2 fragCoord = gl_FragCoord.xy;
  vec2 res = u_resolution;

  // tileDensity = how many tile cells fit across the SHORTER side of the
  // surface, so density reads consistently regardless of aspect ratio.
  float shortSide = min(res.x, res.y);
  float cell = shortSide / max(u_tileDensity, 1.0);
  float gap = cell * 0.06;

  vec2 gridPos = fragCoord / cell;
  vec2 cellId = floor(gridPos);
  vec2 localUv = fract(gridPos) - 0.5;
  vec2 localPx = localUv * cell;

  float tileHalf = (cell - gap) * 0.5;
  float radius = tileHalf * 0.32;

  float d = sdRoundRect(localPx, vec2(tileHalf), radius);

  if (d > 0.0) {
    fragColor = vec4(u_backgroundColor, u_opacity);
    return;
  }

  vec2 rnd = hash22(cellId);

  // --- Always-on ambient bevel shading ---
  // A true 3D surface normal derived from a domed height field, lit by a
  // fixed upper-left studio light. This runs on EVERY tile regardless of
  // the colorful streak, so dark tiles still read as curved glass rather
  // than flat black squares.
  float epsH = 1.2;
  float dCx1 = sdRoundRect(localPx + vec2(epsH, 0.0), vec2(tileHalf), radius);
  float dCx0 = sdRoundRect(localPx - vec2(epsH, 0.0), vec2(tileHalf), radius);
  float dCy1 = sdRoundRect(localPx + vec2(0.0, epsH), vec2(tileHalf), radius);
  float dCy0 = sdRoundRect(localPx - vec2(0.0, epsH), vec2(tileHalf), radius);
  float hR = tileHeight(dCx1, radius);
  float hL = tileHeight(dCx0, radius);
  float hU = tileHeight(dCy1, radius);
  float hD = tileHeight(dCy0, radius);
  vec2 heightGrad = vec2(hR - hL, hU - hD) / (2.0 * epsH);
  vec3 surfaceNormal = normalize(vec3(-heightGrad * 4.0, 1.0));

  vec3 lightDir = normalize(vec3(-0.45, 0.55, 0.7));
  vec3 viewDir = vec3(0.0, 0.0, 1.0);
  vec3 halfDir = normalize(lightDir + viewDir);

  float diffuse = max(dot(surfaceNormal, lightDir), 0.0);
  float specular = pow(max(dot(surfaceNormal, halfDir), 0.0), 14.0);

  // Tinted by the background color rather than neutral gray, so it reads as
  // a hint of glass curvature rather than a separate plastic/keycap layer.
  vec3 ambientBevel = u_backgroundColor * diffuse * 0.16
                     + mix(u_backgroundColor, vec3(0.7, 0.65, 0.78), 0.5) * specular * 0.22;

  // Bevel normal from the rounded-rect SDF gradient, used only to bend
  // (warp) the light sample point near tile edges -- a refraction cue.
  float eps = 1.5;
  float dx = sdRoundRect(localPx + vec2(eps, 0.0), vec2(tileHalf), radius)
           - sdRoundRect(localPx - vec2(eps, 0.0), vec2(tileHalf), radius);
  float dy = sdRoundRect(localPx + vec2(0.0, eps), vec2(tileHalf), radius)
           - sdRoundRect(localPx - vec2(0.0, eps), vec2(tileHalf), radius);
  vec2 gradDir = vec2(dx, dy) / (2.0 * eps);
  float rim = smoothstep(-radius * 1.6, 0.0, d);

  // Per-tile inverse-distance warp: the closer to the tile edge, the more
  // the sample point bends, like light refracting through curved glass.
  float distFromCenter = length(localPx) / max(tileHalf, 0.001);
  float invDist = 1.0 / max(1.0 - distFromCenter * 0.85, 0.15);
  vec2 warpOffset = gradDir * rim * u_warpStrength * invDist * 0.4;

  float t = u_time * u_speed;
  vec2 sampleUv = gridPos + warpOffset;

  float fieldR = lightField(sampleUv + vec2(u_chromaticSpread * 0.6, 0.0), t);
  float fieldG = lightField(sampleUv, t);
  float fieldB = lightField(sampleUv - vec2(u_chromaticSpread * 0.6, 0.0), t);

  float shaped = pow(clamp(fieldG, 0.0, 1.6), u_bandSharpness);
  float shapedR = pow(clamp(fieldR, 0.0, 1.6), u_bandSharpness);
  float shapedB = pow(clamp(fieldB, 0.0, 1.6), u_bandSharpness);

  float variance = mix(0.75, 1.15, rnd.y);
  shaped *= variance;
  shapedR *= variance;
  shapedB *= variance;

  vec3 gradColor = mix(u_colorA, u_colorB, clamp(shaped, 0.0, 1.0));

  vec3 lit = gradColor * shaped;
  float chromaMix = clamp(u_chromaticSpread * 3.0, 0.0, 1.0);
  lit.r = mix(lit.r, gradColor.r * shapedR, chromaMix);
  lit.b = mix(lit.b, gradColor.b * shapedB, chromaMix);

  vec3 color = u_backgroundColor * 0.03 + ambientBevel + lit;

  // Soft recessed contact shadow right at the rounded border
  float innerEdge = smoothstep(-2.5, 0.0, d);
  color *= (1.0 - innerEdge * 0.55);

  fragColor = vec4(color, u_opacity);
}
`

function compileShader(
  gl: WebGL2RenderingContext,
  type: number,
  source: string
): WebGLShader {
  const shader = gl.createShader(type)!
  gl.shaderSource(shader, source)
  gl.compileShader(shader)
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const info = gl.getShaderInfoLog(shader)
    gl.deleteShader(shader)
    throw new Error("Shader compile error: " + info)
  }
  return shader
}

function createProgram(
  gl: WebGL2RenderingContext,
  vertexSrc: string,
  fragmentSrc: string
): WebGLProgram {
  const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc)
  const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc)
  const program = gl.createProgram()!
  gl.attachShader(program, vs)
  gl.attachShader(program, fs)
  gl.linkProgram(program)
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const info = gl.getProgramInfoLog(program)
    gl.deleteProgram(program)
    throw new Error("Program link error: " + info)
  }
  gl.deleteShader(vs)
  gl.deleteShader(fs)
  return program
}

function hexToRgb(hex: string) {
  const clean = (hex || "#000000").replace("#", "")
  const full =
    clean.length === 3
      ? clean
          .split("")
          .map((c) => c + c)
          .join("")
      : clean
  const bigint = parseInt(full, 16) || 0
  const r = ((bigint >> 16) & 255) / 255
  const g = ((bigint >> 8) & 255) / 255
  const b = (bigint & 255) / 255
  return [r, g, b]
}

const clamp = (value: number, min: number, max: number) =>
  Math.min(max, Math.max(min, value))

interface AuroraGlassProps {
  width?: string | number
  height?: string | number
  className?: string
  children?: ReactNode
  speed?: number
  tileDensity?: number
  rippleLayers?: number
  warpStrength?: number
  bandSharpness?: number
  chromaticSpread?: number
  colorA?: string
  colorB?: string
  backgroundColor?: string
  opacity?: number
  dpr?: number
}

export default function AuroraGlass({
  width = "100%",
  height = "100%",
  className = "",
  children,
  speed = 1,
  tileDensity = 4,
  rippleLayers = 6,
  warpStrength = 0.33,
  bandSharpness = 3,
  chromaticSpread = 0,
  colorA = "#1E00FF",
  colorB = "#D765E6",
  backgroundColor = "#FFFFFF",
  opacity = 1,
  dpr = 1.5,
}: AuroraGlassProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const rafRef = useRef(0)

  // Keep the latest prop values in a ref so the render loop (started once)
  // always reads current values without needing to be torn down and
  // rebuilt on every prop change.
  interface PropsSnapshot {
    speed: number
    tileDensity: number
    rippleLayers: number
    warpStrength: number
    bandSharpness: number
    chromaticSpread: number
    colorA: string
    colorB: string
    backgroundColor: string
    opacity: number
  }
  const propsRef = useRef<PropsSnapshot>({} as PropsSnapshot)
  propsRef.current = {
    speed,
    tileDensity: clamp(tileDensity, 1, 16),
    rippleLayers: Math.round(clamp(rippleLayers, 1, MAX_RIPPLE_LAYERS)),
    warpStrength: clamp(warpStrength, 0, 0.6),
    bandSharpness: clamp(bandSharpness, 0.5, 10),
    chromaticSpread: clamp(chromaticSpread, 0, 1),
    colorA,
    colorB,
    backgroundColor,
    opacity: clamp(opacity, 0, 1),
  }

  const dprRef = useRef(clamp(dpr, 1, 3))
  dprRef.current = clamp(dpr, 1, 3)

  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const gl = canvas.getContext("webgl2", { antialias: true, alpha: true })
    if (!gl) {
      console.warn(
        "WebGL2 is not supported in this browser; AuroraGlass cannot render."
      )
      return
    }

    let program: WebGLProgram
    try {
      program = createProgram(gl, VERTEX_SRC, FRAGMENT_SRC)
    } catch (err) {
      console.error(err)
      return
    }
    gl.useProgram(program)
    gl.enable(gl.BLEND)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

    const positionBuffer = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 3, -1, -1, 3]),
      gl.STATIC_DRAW
    )
    const positionLoc = gl.getAttribLocation(program, "a_position")
    gl.enableVertexAttribArray(positionLoc)
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0)

    const u: Record<string, WebGLUniformLocation | null> = {}
    ;[
      "u_resolution",
      "u_time",
      "u_speed",
      "u_tileDensity",
      "u_rippleLayers",
      "u_warpStrength",
      "u_bandSharpness",
      "u_chromaticSpread",
      "u_colorA",
      "u_colorB",
      "u_backgroundColor",
      "u_opacity",
    ].forEach((name) => {
      u[name] = gl.getUniformLocation(program, name)
    })

    let w = 0
    let h = 0

    function resize() {
      const cv = canvas!
      const parent = cv.parentElement
      const rect = parent
        ? parent.getBoundingClientRect()
        : cv.getBoundingClientRect()
      const ratio = dprRef.current
      w = Math.max(1, Math.floor(rect.width * ratio))
      h = Math.max(1, Math.floor(rect.height * ratio))
      if (cv.width !== w || cv.height !== h) {
        cv.width = w
        cv.height = h
      }
      gl!.viewport(0, 0, w, h)
    }

    const resizeObserver = new ResizeObserver(resize)
    if (canvas.parentElement) resizeObserver.observe(canvas.parentElement)
    resize()

    const start = performance.now()

    function frame(now: number) {
      const t = (now - start) / 1000
      const p = propsRef.current
      const g = gl!

      g.useProgram(program)
      g.clearColor(0, 0, 0, 0)
      g.clear(g.COLOR_BUFFER_BIT)

      g.uniform2f(u.u_resolution, w, h)
      g.uniform1f(u.u_time, t)
      g.uniform1f(u.u_speed, p.speed)
      g.uniform1f(u.u_tileDensity, p.tileDensity)
      g.uniform1i(u.u_rippleLayers, p.rippleLayers)
      g.uniform1f(u.u_warpStrength, p.warpStrength)
      g.uniform1f(u.u_bandSharpness, p.bandSharpness)
      g.uniform1f(u.u_chromaticSpread, p.chromaticSpread)
      g.uniform3fv(u.u_colorA, hexToRgb(p.colorA))
      g.uniform3fv(u.u_colorB, hexToRgb(p.colorB))
      g.uniform3fv(u.u_backgroundColor, hexToRgb(p.backgroundColor))
      g.uniform1f(u.u_opacity, p.opacity)

      g.drawArrays(g.TRIANGLES, 0, 3)
      rafRef.current = requestAnimationFrame(frame)
    }

    rafRef.current = requestAnimationFrame(frame)

    return () => {
      cancelAnimationFrame(rafRef.current)
      resizeObserver.disconnect()
      gl.deleteProgram(program)
      gl.deleteBuffer(positionBuffer)
    }
    // Render loop is started once; per-frame values are read from propsRef
    // and dprRef so prop changes don't require tearing down the GL context.
  }, [])

  return (
    <div
      className={className}
      style={{
        position: "relative",
        width,
        height,
        overflow: "hidden",
      }}
    >
      <canvas
        ref={canvasRef}
        style={{
          position: "absolute",
          inset: 0,
          display: "block",
          width: "100%",
          height: "100%",
        }}
      />
      {children != null && (
        <div style={{ position: "relative", width: "100%", height: "100%" }}>
          {children}
        </div>
      )}
    </div>
  )
}

More from gammaui

Browse all components in this library →