pnpm dlx shadcn@latest add @uigrove/grain-gradient

Grain Gradient

A WebGL surface with a curved, diffused edge between three colors, film grain and a slow breathing motion.

Installation
pnpm dlx shadcn@latest add @uigrove/grain-gradient

Dependencies: none. Installs @uigrove/dot-canvas alongside.

First time? Add the registry to components.json
{
  "registries": {
    "@uigrove": "https://uigrove.me/r/{name}.json"
  }
}
Usage
import { GrainGradient } from '@/components/uigrove/grain-gradient'

<GrainGradient className="absolute inset-0" />
Props
NameTypeDefaultDescription
colorLightstring'#e4e4e8'Highlight color
colorMidstring'#6f6f78'Wash and diffused edge color
colorDarkstring'#09090c'Shadow color
anglenumber0Rotation of the composition
positionnumber-0.25Horizontal offset of the edge
curvenumber0.48Curvature of the shadow edge
softnessnumber0.18Width of the diffusion
scalenumber1Zoom of the composition
grainnumber0.32Film grain intensity
grainSizenumber1Grain size in CSS pixels
seednumber1Reproducible grain pattern
speednumber1Breathing speed, 0 freezes the surface
classNamestring—Classes for the root element. Use it for sizing and position.
Code
'use client'

import { useEffect, useRef, type CSSProperties } from 'react'
import { useDotCanvas } from '@/hooks/use-dot-canvas'
import { GrainGradientEngine, type GrainGradientOptions } from './grain-gradient.engine'

export interface GrainGradientProps extends Partial<GrainGradientOptions> {
  className?: string
  style?: CSSProperties
}

export const grainGradientDefaults: GrainGradientOptions = {
  colorLight: '#e4e4e8',
  colorMid: '#6f6f78',
  colorDark: '#09090c',
  angle: 0,
  position: -0.25,
  curve: 0.48,
  softness: 0.18,
  scale: 1,
  grain: 0.32,
  grainSize: 1,
  seed: 1,
  speed: 1,
}

export function GrainGradient({
  colorLight = grainGradientDefaults.colorLight,
  colorMid = grainGradientDefaults.colorMid,
  colorDark = grainGradientDefaults.colorDark,
  angle = grainGradientDefaults.angle,
  position = grainGradientDefaults.position,
  curve = grainGradientDefaults.curve,
  softness = grainGradientDefaults.softness,
  scale = grainGradientDefaults.scale,
  grain = grainGradientDefaults.grain,
  grainSize = grainGradientDefaults.grainSize,
  seed = grainGradientDefaults.seed,
  speed = grainGradientDefaults.speed,
  className,
  style,
}: GrainGradientProps) {
  const engineRef = useRef<GrainGradientEngine | null>(null)
  if (!engineRef.current) engineRef.current = new GrainGradientEngine(grainGradientDefaults)
  const engine = engineRef.current
  const { containerRef, canvasRef, wake } = useDotCanvas(engine, { pointer: false, kind: 'webgl' })

  useEffect(() => {
    engine.setOptions({
      colorLight,
      colorMid,
      colorDark,
      angle,
      position,
      curve,
      softness,
      scale,
      grain,
      grainSize,
      seed,
      speed,
    })
    wake()
  }, [
    engine,
    wake,
    colorLight,
    colorMid,
    colorDark,
    angle,
    position,
    curve,
    softness,
    scale,
    grain,
    grainSize,
    seed,
    speed,
  ])

  const fallback = `linear-gradient(${90 + angle}deg, ${colorDark} 0%, ${colorMid} 55%, ${colorLight} 100%)`

  return (
    <div
      ref={containerRef}
      className={['uig-grain-gradient', 'pointer-events-none overflow-hidden', className]
        .filter(Boolean)
        .join(' ')}
      style={{ background: fallback, ...style }}
      aria-hidden="true"
    >
      <canvas ref={canvasRef} className="block h-full w-full" />
    </div>
  )
}