# Gradient Orb

> Glowing shader orb with noise-based color mixing, hue rotation, and breathing pulse

A shadcn/ui block from the [@uicapsule](https://uicapsule.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/gradient-orb).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/gradient-orb
- Screenshot: https://shadcn.exchange/shot/uicapsule/gradient-orb/light.png
- Registry item: https://uicapsule.com/r/gradient-orb.json
- Categories: effects, geometric
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uicapsule/gradient-orb.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@react-three/fiber`, `three`

## Files

### `/gradient-orb.tsx`

````tsx
"use client";

import { useRef, useMemo, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";

/**
 * Configuration options for the gradient orb.
 * All fields are optional and fall back to sensible defaults.
 */
export interface GradientOrbConfig {
  /** CSS color string for the canvas background. @default "#0a0a0a" */
  background?: string;
  /** Hue rotation in degrees applied to all gradient colors. @default 0 */
  hue?: number;
  /** Constant rotation speed of the orb (radians/sec). @default 0.3 */
  rotationSpeed?: number;
  /** Scale of the noise pattern inside the orb. @default 0.65 */
  noiseScale?: number;
  /** Inner radius of the orb glow (0–1). @default 0.1 */
  innerRadius?: number;
}

const defaults: Required<GradientOrbConfig> = {
  background: "#0a0a0a",
  hue: 0,
  innerRadius: 0.1,
  noiseScale: 0.65,
  rotationSpeed: 0.3,
};

/**
 * GLSL vertex shader — pass-through that outputs clip-space positions
 * directly from a fullscreen triangle in NDC.
 */
const vertexShader = `
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = vec4(position.xy, 0.0, 1.0);
  }
`;

/**
 * GLSL fragment shader — renders a glowing orb with three noise-mixed colors,
 * hue rotation, breathing pulse, and constant rotation.
 */
const fragmentShader = `
  precision highp float;

  uniform float iTime;
  uniform vec3 iResolution;
  uniform float hue;
  uniform float rot;
  uniform float noiseScale;
  uniform float innerRadius;

  varying vec2 vUv;

  // --- YIQ color space hue rotation ---

  vec3 rgb2yiq(vec3 c) {
    return vec3(
      dot(c, vec3(0.299, 0.587, 0.114)),
      dot(c, vec3(0.596, -0.274, -0.322)),
      dot(c, vec3(0.211, -0.523, 0.312))
    );
  }

  vec3 yiq2rgb(vec3 c) {
    return vec3(
      c.x + 0.956 * c.y + 0.621 * c.z,
      c.x - 0.272 * c.y - 0.647 * c.z,
      c.x - 1.106 * c.y + 1.703 * c.z
    );
  }

  vec3 adjustHue(vec3 color, float hueDeg) {
    float hueRad = radians(hueDeg);
    vec3 yiq = rgb2yiq(color);
    float cosA = cos(hueRad);
    float sinA = sin(hueRad);
    yiq.yz = vec2(yiq.y * cosA - yiq.z * sinA, yiq.y * sinA + yiq.z * cosA);
    return yiq2rgb(yiq);
  }

  // --- 3D simplex noise (hash-based) ---

  vec3 hash33(vec3 p3) {
    p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
    p3 += dot(p3, p3.yxz + 19.19);
    return -1.0 + 2.0 * fract(vec3(p3.x + p3.y, p3.x + p3.z, p3.y + p3.z) * p3.zyx);
  }

  float snoise3(vec3 p) {
    const float K1 = 0.333333333;
    const float K2 = 0.166666667;
    vec3 i = floor(p + (p.x + p.y + p.z) * K1);
    vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
    vec3 e = step(vec3(0.0), d0 - d0.yzx);
    vec3 i1 = e * (1.0 - e.zxy);
    vec3 i2 = 1.0 - e.zxy * (1.0 - e);
    vec3 d1 = d0 - (i1 - K2);
    vec3 d2 = d0 - (i2 - K1);
    vec3 d3 = d0 - 0.5;
    vec4 h = max(0.6 - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0);
    vec4 n = h * h * h * h * vec4(
      dot(d0, hash33(i)),
      dot(d1, hash33(i + i1)),
      dot(d2, hash33(i + i2)),
      dot(d3, hash33(i + 1.0))
    );
    return dot(vec4(31.316), n);
  }

  // --- Orb rendering ---

  vec4 extractAlpha(vec3 colorIn) {
    float a = max(max(colorIn.r, colorIn.g), colorIn.b);
    return vec4(colorIn.rgb / (a + 1e-5), a);
  }

  const vec3 baseColor0 = vec3(0.239, 0.353, 1.0);   // blue
  const vec3 baseColor1 = vec3(0.616, 0.0, 1.0);     // purple
  const vec3 baseColor2 = vec3(1.0, 0.373, 0.122);   // orange
  const vec3 baseColor3 = vec3(0.0, 0.0, 0.0);       // black

  float light1(float intensity, float attenuation, float dist) {
    return intensity / (1.0 + dist * attenuation);
  }

  float light2(float intensity, float attenuation, float dist) {
    return intensity / (1.0 + dist * dist * attenuation);
  }

  vec4 draw(vec2 uv) {
    vec3 color0 = adjustHue(baseColor0, hue);
    vec3 color1 = adjustHue(baseColor1, hue);
    vec3 color2 = adjustHue(baseColor2, hue);
    vec3 color3 = adjustHue(baseColor3, hue);

    float len = length(uv);
    float invLen = len > 0.0 ? 1.0 / len : 0.0;

    float pulse = sin(iTime * 1.5) * 0.02;

    float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;

    float r0 = mix(mix(innerRadius + pulse, 1.0, 0.4), mix(innerRadius + pulse, 1.0, 0.6), n0);

    float d0 = distance(uv, (r0 * invLen) * uv);
    float v0 = light1(1.0, 10.0, d0);
    v0 *= smoothstep(r0 * 1.05, r0, len);
    float cl = cos(atan(uv.y, uv.x) + iTime * 2.0) * 0.5 + 0.5;

    float a = iTime * -1.0;
    vec2 pos = vec2(cos(a), sin(a)) * r0;
    float d = distance(uv, pos);
    float v1 = light2(1.5, 5.0, d);
    v1 *= light1(1.0, 50.0, d0);

    float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
    float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);

    vec3 col = mix(color1, color2, cl);
    col = mix(col, color0, n0);
    col = mix(color3, col, v0);
    col = (col + v1) * v2 * v3;
    col = clamp(col, 0.0, 1.0);

    return extractAlpha(col);
  }

  void main() {
    vec2 center = iResolution.xy * 0.5;
    float size = min(iResolution.x, iResolution.y);
    vec2 uv = (vUv * iResolution.xy - center) / size * 2.0;

    float s = sin(rot);
    float c = cos(rot);
    uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);

    vec4 col = draw(uv);
    gl_FragColor = vec4(col.rgb * col.a, col.a);
  }
`;

/** Internal scene component for the gradient fullscreen shader. */
const GradientScene = ({
  hue,
  rotationSpeed,
  noiseScale,
  innerRadius,
}: Required<Omit<GradientOrbConfig, "background">>) => {
  const materialRef = useRef<THREE.ShaderMaterial>(null);
  const { size, viewport } = useThree();
  const rotRef = useRef(0);
  const lastTimeRef = useRef(0);

  const geometry = useMemo(() => {
    const geo = new THREE.BufferGeometry();
    geo.setAttribute(
      "position",
      new THREE.Float32BufferAttribute([-1, -1, 0, 3, -1, 0, -1, 3, 0], 3),
    );
    geo.setAttribute("uv", new THREE.Float32BufferAttribute([0, 0, 2, 0, 0, 2], 2));
    return geo;
  }, []);

  useEffect(() => () => geometry.dispose(), [geometry]);

  const uniforms = useMemo(
    () => ({
      hue: { value: hue },
      iResolution: { value: new THREE.Vector3(1, 1, 1) },
      iTime: { value: 0 },
      innerRadius: { value: innerRadius },
      noiseScale: { value: noiseScale },
      rot: { value: 0 },
    }),
    [hue, noiseScale, innerRadius],
  );

  useFrame((state) => {
    if (!materialRef.current) {
      return;
    }

    const t = state.clock.elapsedTime;
    const dt = t - lastTimeRef.current;
    lastTimeRef.current = t;

    rotRef.current += dt * rotationSpeed;

    const u = materialRef.current.uniforms;
    if (!u.iTime || !u.hue || !u.rot || !u.iResolution) {
      return;
    }
    u.iTime.value = t;
    u.hue.value = hue;
    u.rot.value = rotRef.current;
    u.iResolution.value.set(
      size.width * viewport.dpr,
      size.height * viewport.dpr,
      size.width / size.height,
    );
  });

  return (
    <mesh geometry={geometry} frustumCulled={false}>
      <shaderMaterial
        ref={materialRef}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
        uniforms={uniforms}
        transparent
        depthWrite={false}
        depthTest={false}
      />
    </mesh>
  );
};

/**
 * Glowing shader orb with noise-based 3-color mixing, YIQ hue rotation,
 * breathing pulse, and constant rotation — all rendered on GPU as a single
 * fullscreen triangle.
 *
 * @example
 * ```tsx
 * <GradientOrb />
 * <GradientOrb config={{ hue: 120, rotationSpeed: 0.5 }} />
 * ```
 */
export const GradientOrb = ({
  config,
  className = "",
}: {
  config?: GradientOrbConfig;
  className?: string;
}) => {
  const { background, hue, rotationSpeed, noiseScale, innerRadius } = {
    ...defaults,
    ...config,
  };

  return (
    <div className={`w-full h-full ${className}`} style={{ background }}>
      {/* Camera is unused — the vertex shader outputs clip-space positions directly */}
      <Canvas gl={{ alpha: false, antialias: true }}>
        <color attach="background" args={[background]} />
        <GradientScene
          hue={hue}
          rotationSpeed={rotationSpeed}
          noiseScale={noiseScale}
          innerRadius={innerRadius}
        />
      </Canvas>
    </div>
  );
};
````

### `/preview.tsx`

```tsx
"use client";

import { GradientOrb } from "./gradient-orb";

const Preview = () => (
  <div className="h-screen w-full">
    <GradientOrb />
  </div>
);

export default Preview;
```
