shadimage
React Bits
componentAcidSquares-JS-CSSsource: ok · hit

AcidSquares

A crystalline corridor of stacked squares receding into depth.

npx shadcn add "https://reactbits.dev/r/AcidSquares-JS-CSS.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ogl@^1.0.11

Files2 · source cached 10/14/2026, 12:01:10 PM

AcidSquares.css
.acid-squares-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.acid-squares-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
AcidSquares.jsx
'use client';

import { useEffect, useRef } from 'react';

import './AcidSquares.css';

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

const buildFragment = (square, light, twist) => `#version 300 es
#define SQUARE ${square}
#define LIGHT ${light}
#define TWIST ${twist}
precision highp float;
out vec4 outColor;
uniform vec2 uResolution;
uniform float uTime;
uniform float uSteps;
uniform float uDepth;
uniform float uSpread;
uniform float uLine;
uniform float uZoom;
uniform float uTravel;
uniform float uTwist;
uniform float uRoll;
uniform vec4 uPulse;
uniform vec2 uSparkle;
uniform vec3 uColor;
uniform vec3 uAccent;
uniform float uBrightness;
uniform vec4 uTorch;
uniform vec2 uLook;
uniform float uGrain;
uniform float uFade;
uniform float uOpacity;

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

mat2 rotate(float a) {
  float c = cos(a);
  float s = sin(a);
  return mat2(c, -s, s, c);
}

float smootherstep(float edge, float x) {
  float t = clamp(x / max(edge, 0.0001), 0.0, 1.0);
  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

void main() {
  vec2 ndc = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  vec2 dir = rotate(uRoll) * ndc * (0.5 / max(uZoom, 0.05)) + uLook;
  vec3 offset = vec3(0.0, 0.0, uTravel);
  vec3 p = vec3(0.0);
  float s = 0.0;
  float glow = 0.0;
  float edgeLine = 0.0;
  float edgeSoft = 0.0;
  float pulse = 0.0;
  float crest = 0.0;
  float peak = 0.0;
  vec3 home = vec3(0.0);

  for (int i = 0; i < 64; i++) {
    if (float(i) >= uSteps) break;
    p += vec3(dir * s, s);
    vec3 q = p + offset;
#if TWIST
    q.xy = rotate(q.z * uTwist) * q.xy;
#endif
    vec2 cell = ceil(q.xy);
#if SQUARE
    float tube = max(abs(q.x), abs(q.z));
    float ring = max(abs(cell.x), abs(cell.y));
#else
    float tube = length(q.xz);
    float ring = length(cell);
#endif
    s += uDepth - tube + length(cell);
    s = 0.002 + abs(s) * uSpread;
    float g = 1.0 / s;
    glow += g;
    vec2 seam = abs(fract(q.xy) - 0.5);
    float gap = 0.5 - max(seam.x, seam.y);
    gap *= gap;
    float line = max(1.0 - gap * 156.25, 0.0);
    line *= line;
    edgeLine += g * line * line;
#if LIGHT
    float soft = max(1.0 - gap * 12.5, 0.0);
    soft *= soft;
    edgeSoft += g * soft * soft;
#endif
    float wave = fract(ring * uPulse.y - uPulse.w) - 0.5;
    float band = g * exp(wave > 0.0 ? -wave * wave / (uPulse.z * uPulse.z) : wave / (uPulse.z * 2.5)) * smoothstep(0.6, 1.8, ring);
    pulse += band;
    crest += wave > -uPulse.z ? band : 0.0;
    if (g > peak) {
      peak = g;
      home = vec3(cell, floor(q.z * 0.5));
    }
  }

  float h = hash(home.xy * 7.13 + home.z);
  float flicker = step(1.0 - uSparkle.x, h) * (0.5 + 0.5 * sin(uSparkle.y + h * 31.0));
  float base = tanh(glow / 2700.0 * uBrightness);
  float lines = tanh(edgeLine / 2700.0 * uBrightness * 2.0);
  float lit = tanh(pulse / 2700.0 * uBrightness * 1.6);
  float front = tanh(crest / 2700.0 * uBrightness * 1.6);
  float twinkle = tanh(glow * flicker / 2700.0 * uBrightness * 1.6);
  vec2 toTorch = ndc - uTorch.xy;
  float torch = exp(-dot(toTorch, toTorch) / max(uTorch.z * uTorch.z, 0.0001)) * uTorch.w;
  float spark = clamp(front * uPulse.x + twinkle + base * torch * 1.4, 0.0, 1.0);
  float haze = max(lit - front, 0.0) * uPulse.x;

  vec2 uv = gl_FragCoord.xy / uResolution;
  float edge = uFade * 0.5;
  float mask = smootherstep(edge, uv.x) * smootherstep(edge, 1.0 - uv.x) * smootherstep(edge, uv.y) * smootherstep(edge, 1.0 - uv.y);
  float grain = (hash(gl_FragCoord.xy + fract(uTime * 7.31) * 113.0) - 0.5) * uGrain;
  mask *= uOpacity;

#if LIGHT
  float soft = tanh(edgeSoft / 2700.0 * uBrightness * 1.2);
  float near = mix(0.2, 1.0, smoothstep(0.15, 1.1, length(ndc)));
  float face = smoothstep(0.3, 1.0, base) * 0.12;
  float shade = (1.0 - (1.0 - face) * (1.0 - soft * 0.25) * (1.0 - lines * uLine)) * near;
  float glowInk = clamp((1.0 - exp(-spark * 2.4)) * 0.9 + (1.0 - exp(-haze * 2.4)) * 0.16, 0.0, 1.0);
  vec3 color = uColor * 0.5 * shade + uAccent * glowInk * (1.0 - shade);
  float alpha = shade + glowInk * (1.0 - shade);
  vec3 tint = color / max(alpha, 0.0001);
  alpha = clamp(alpha * (1.0 + grain), 0.0, 1.0) * mask;
  outColor = vec4(tint * alpha, alpha);
#else
  float lift = smoothstep(0.0, 0.6, base);
  vec3 structure = mix(uColor, vec3(1.0), 0.22 + 0.13 * lift) * mix(0.45, 1.0, lift);
  structure = mix(structure, mix(uColor, vec3(1.0), 0.55), smoothstep(0.65, 1.0, base));
  float accent = clamp(spark + haze * 0.6, 0.0, 1.0);
  vec3 color = structure * base + mix(uColor, vec3(1.0), 0.7) * lines * uLine + uAccent * accent * (0.6 + 0.4 * base);
  color = max(color + grain * max(color.r, max(color.g, color.b)), 0.0) * mask;
  outColor = vec4(color, max(color.r, max(color.g, color.b)));
#endif
}`;

const UNIFORMS = [
  'uResolution',
  'uTime',
  'uSteps',
  'uDepth',
  'uSpread',
  'uLine',
  'uZoom',
  'uTravel',
  'uTwist',
  'uRoll',
  'uPulse',
  'uSparkle',
  'uColor',
  'uAccent',
  'uBrightness',
  'uTorch',
  'uLook',
  'uGrain',
  'uFade',
  'uOpacity'
];

const DETAIL_STEPS = { low: 20, medium: 32, high: 48 };
const MAX_RENDER_DIM = 2400;

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

const compile = (gl, type, source) => {
  const shader = gl.createShader(type);
  if (!shader) return null;
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;
  gl.deleteShader(shader);
  return null;
};

const link = (gl, vertexSource, fragmentSource) => {
  const vertex = compile(gl, gl.VERTEX_SHADER, vertexSource);
  const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource);
  const program = gl.createProgram();
  if (!vertex || !fragment || !program) return null;
  gl.attachShader(program, vertex);
  gl.attachShader(program, fragment);
  gl.bindAttribLocation(program, 0, 'aPosition');
  gl.linkProgram(program);
  gl.deleteShader(vertex);
  gl.deleteShader(fragment);
  if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;
  gl.deleteProgram(program);
  return null;
};

const locate = (gl, program, names) => {
  const result = {};
  for (const name of names) result[name] = gl.getUniformLocation(program, name);
  return result;
};

const AcidSquares = ({
  color = '#120f17',
  accentColor = '#c6ff3d',
  shape = 'square',
  pulse = 1,
  pulseSpeed = 0.45,
  pulseSpacing = 1,
  pulseWidth = 1,
  sparkle = 0.04,
  speed = 0.7,
  breathe = 1,
  zoom = 1.3,
  depth = 10,
  edges = 0.45,
  twist = 0,
  roll = 0,
  brightness = 1,
  mouseInteraction = true,
  mouseStrength = 1,
  mouseRadius = 0.35,
  detail = 'medium',
  grain = 0.03,
  fade = 0,
  opacity = 1,
  lightMode = false,
  paused = false,
  dpr,
  ...rest
}) => {
  const containerRef = useRef(null);
  const canvasRef = useRef(null);
  const wakeRef = useRef(null);
  const settings = {
    color: String(color),
    accentColor: String(accentColor),
    shape: shape === 'round' ? 0 : 1,
    pulse: Math.max(0, pulse),
    pulseSpeed,
    pulseSpacing: clamp(pulseSpacing, 0.2, 5),
    pulseWidth: clamp(pulseWidth, 0.1, 5),
    sparkle: clamp(sparkle, 0, 1),
    speed,
    breathe: Math.max(0, breathe),
    zoom: clamp(zoom, 0.3, 5),
    depth: clamp(depth, 2, 24),
    edges: clamp(edges, 0, 1),
    twist,
    roll,
    brightness: Math.max(0, brightness),
    mouseInteraction,
    mouseStrength: Math.max(0, mouseStrength),
    mouseRadius: clamp(mouseRadius, 0.02, 2),
    steps: DETAIL_STEPS[detail] || DETAIL_STEPS.medium,
    grain: Math.max(0, grain),
    fade: clamp(fade, 0, 1),
    opacity: clamp(opacity, 0, 1),
    lightMode,
    paused,
    dpr
  };
  const settingsRef = useRef(settings);
  settingsRef.current = settings;

  useEffect(() => {
    const container = containerRef.current;
    const canvas = canvasRef.current;
    const gl = canvas?.getContext('webgl2', {
      alpha: true,
      premultipliedAlpha: true,
      antialias: false,
      depth: false,
      stencil: false
    });
    if (!container || !canvas || !gl) return undefined;

    const programs = new Map();
    const pick = s => {
      const key = `${s.shape}${s.lightMode ? 1 : 0}${s.twist !== 0 ? 1 : 0}`;
      if (!programs.has(key)) {
        const program = link(gl, VERTEX, buildFragment(s.shape, s.lightMode ? 1 : 0, s.twist !== 0 ? 1 : 0));
        programs.set(key, program ? { program, uniforms: locate(gl, program, UNIFORMS) } : null);
      }
      return programs.get(key);
    };
    if (!pick(settingsRef.current)) return undefined;

    const vao = gl.createVertexArray();
    const buffer = gl.createBuffer();
    gl.bindVertexArray(vao);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
    gl.bindVertexArray(null);

    const probe = document.createElement('canvas');
    probe.width = 1;
    probe.height = 1;
    const probeContext = probe.getContext('2d', { willReadFrequently: true });
    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
    const state = {
      time: 0,
      pulsePhase: 0,
      sparklePhase: 0,
      roll: 0,
      width: 1,
      height: 1,
      colorKey: '',
      color: [0.07, 0.06, 0.09],
      accent: [0.78, 1, 0.24],
      pointer: { x: 0, y: 0, inside: false },
      torch: { x: 0, y: 0, vx: 0, vy: 0, presence: 0, placed: false },
      look: { x: 0, y: 0, vx: 0, vy: 0 }
    };
    let raf = 0;
    let last = 0;
    let visible = true;
    let alive = true;

    const toRgb = (value, fallback) => {
      if (!probeContext || !value) return fallback;
      const css = /^[0-9a-f]{3,8}$/i.test(value) ? `#${value}` : value;
      probeContext.clearRect(0, 0, 1, 1);
      probeContext.fillStyle = '#000000';
      probeContext.fillStyle = css;
      probeContext.fillRect(0, 0, 1, 1);
      const [r, g, b] = probeContext.getImageData(0, 0, 1, 1).data;
      return [r / 255, g / 255, b / 255];
    };

    const resolveColors = s => {
      const key = `${s.color}|${s.accentColor}`;
      if (key === state.colorKey) return;
      state.colorKey = key;
      state.color = toRgb(s.color, [0.07, 0.06, 0.09]);
      state.accent = toRgb(s.accentColor, [0.78, 1, 0.24]);
    };

    const resize = () => {
      const s = settingsRef.current;
      const width = Math.max(1, container.clientWidth);
      const height = Math.max(1, container.clientHeight);
      const baseDpr = Math.min(s.dpr || window.devicePixelRatio || 1, 2);
      const longest = Math.max(width, height) * baseDpr;
      const ratio = longest > MAX_RENDER_DIM ? (baseDpr * MAX_RENDER_DIM) / longest : baseDpr;
      const pixelWidth = Math.max(1, Math.round(width * ratio));
      const pixelHeight = Math.max(1, Math.round(height * ratio));
      if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
        canvas.width = pixelWidth;
        canvas.height = pixelHeight;
      }
      state.width = width;
      state.height = height;
      render();
    };

    const render = () => {
      const s = settingsRef.current;
      const entry = pick(s);
      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      if (!entry) return;
      const { uniforms } = entry;
      resolveColors(s);
      const aspect = state.width / state.height;
      gl.useProgram(entry.program);
      gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);
      gl.uniform1f(uniforms.uTime, state.time % 1000);
      gl.uniform1f(uniforms.uSteps, s.steps);
      gl.uniform1f(uniforms.uDepth, s.depth);
      gl.uniform1f(uniforms.uSpread, 0.2 + s.edges * 0.22);
      gl.uniform1f(uniforms.uLine, s.lightMode ? 0.25 + s.edges * 0.45 : 0.15 + s.edges * 0.35);
      gl.uniform1f(uniforms.uZoom, s.zoom);
      gl.uniform1f(uniforms.uTravel, Math.sin(state.time) * s.breathe);
      gl.uniform1f(uniforms.uTwist, s.twist);
      gl.uniform1f(uniforms.uRoll, state.roll);
      gl.uniform4f(uniforms.uPulse, s.pulse, 0.16 / s.pulseSpacing, 0.05 * s.pulseWidth, state.pulsePhase);
      gl.uniform2f(uniforms.uSparkle, s.sparkle, state.sparklePhase);
      gl.uniform3f(uniforms.uColor, state.color[0], state.color[1], state.color[2]);
      gl.uniform3f(uniforms.uAccent, state.accent[0], state.accent[1], state.accent[2]);
      gl.uniform1f(uniforms.uBrightness, s.brightness);
      gl.uniform4f(
        uniforms.uTorch,
        (state.torch.x / state.width - 0.5) * 2 * aspect,
        (0.5 - state.torch.y / state.height) * 2,
        s.mouseRadius,
        s.mouseStrength * state.torch.presence
      );
      gl.uniform2f(uniforms.uLook, state.look.x, state.look.y);
      gl.uniform1f(uniforms.uGrain, s.grain);
      gl.uniform1f(uniforms.uFade, s.fade);
      gl.uniform1f(uniforms.uOpacity, s.opacity);
      gl.bindVertexArray(vao);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      gl.bindVertexArray(null);
    };

    const frame = now => {
      raf = 0;
      if (!alive || !visible || document.hidden) return;
      const s = settingsRef.current;
      const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
      last = now;
      const moving = !s.paused && !reduce;
      if (moving) {
        state.time += dt * s.speed;
        state.pulsePhase = (state.pulsePhase + dt * s.pulseSpeed) % 1;
        state.sparklePhase = (state.sparklePhase + dt * 2) % (Math.PI * 2);
        state.roll += dt * s.roll;
      }

      const { pointer, torch, look } = state;
      const active = s.mouseInteraction && pointer.inside && !reduce;
      if (active && !torch.placed) {
        torch.x = pointer.x;
        torch.y = pointer.y;
        torch.vx = 0;
        torch.vy = 0;
        torch.placed = true;
      }
      torch.vx += (110 * (pointer.x - torch.x) - 19 * torch.vx) * dt;
      torch.vy += (110 * (pointer.y - torch.y) - 19 * torch.vy) * dt;
      torch.x += torch.vx * dt;
      torch.y += torch.vy * dt;
      const goal = active ? 1 : 0;
      torch.presence += (goal - torch.presence) * (1 - Math.exp(-dt / (goal > torch.presence ? 0.3 : 0.6)));
      if (!active && torch.presence < 0.001) torch.placed = false;
      const lookX = active ? (pointer.x / state.width - 0.5) * 0.06 : 0;
      const lookY = active ? (0.5 - pointer.y / state.height) * 0.06 : 0;
      look.vx += (40 * (lookX - look.x) - 12 * look.vx) * dt;
      look.vy += (40 * (lookY - look.y) - 12 * look.vy) * dt;
      look.x += look.vx * dt;
      look.y += look.vy * dt;

      render();
      const settling =
        Math.abs(goal - torch.presence) > 0.002 ||
        Math.hypot(torch.vx, torch.vy) > 0.5 ||
        Math.abs(lookX - look.x) > 0.0002 ||
        Math.abs(lookY - look.y) > 0.0002;
      if (moving || settling) raf = requestAnimationFrame(frame);
      else last = 0;
    };

    const wake = () => {
      if (!raf && alive && visible && !document.hidden) raf = requestAnimationFrame(frame);
    };

    const onPointerMove = event => {
      const rect = container.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      state.pointer.x = x;
      state.pointer.y = y;
      state.pointer.inside = x >= 0 && y >= 0 && x <= rect.width && y <= rect.height;
      wake();
    };

    const onPointerLeave = () => {
      state.pointer.inside = false;
      wake();
    };

    const onVisibility = () => {
      last = 0;
      wake();
    };

    const resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(container);
    const intersection = new IntersectionObserver(entries => {
      visible = entries.some(entry => entry.isIntersecting);
      if (visible) {
        last = 0;
        wake();
      }
    });
    intersection.observe(container);
    window.addEventListener('pointermove', onPointerMove, { passive: true });
    document.documentElement.addEventListener('pointerleave', onPointerLeave);
    document.addEventListener('visibilitychange', onVisibility);
    resize();
    wake();

    wakeRef.current = () => {
      if (!raf) render();
      wake();
    };

    return () => {
      alive = false;
      cancelAnimationFrame(raf);
      wakeRef.current = null;
      resizeObserver.disconnect();
      intersection.disconnect();
      window.removeEventListener('pointermove', onPointerMove);
      document.documentElement.removeEventListener('pointerleave', onPointerLeave);
      document.removeEventListener('visibilitychange', onVisibility);
      gl.deleteBuffer(buffer);
      gl.deleteVertexArray(vao);
      programs.forEach(entry => entry && gl.deleteProgram(entry.program));
    };
  }, []);

  useEffect(() => {
    wakeRef.current?.();
  });

  const { className = '', ...attributes } = rest;

  return (
    <div ref={containerRef} className={`acid-squares-container${className ? ` ${className}` : ''}`} {...attributes}>
      <canvas ref={canvasRef} className="acid-squares-canvas" aria-hidden="true" />
    </div>
  );
};

export default AcidSquares;

More from React Bits

Browse all components in this library →