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

Film Grain

An animated film grain overlay for any section, drawn on the GPU and blended with what sits beneath it.

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

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 { FilmGrain } from '@/components/uigrove/film-grain'

<FilmGrain className="absolute inset-0" />
Props
NameTypeDefaultDescription
intensitynumber0.5Contrast of the grain against the surface
sizenumber1Grain cell size in CSS pixels
speednumber1How fast cells re-roll, 0 freezes the grain
blend'overlay' | 'soft-light' | 'screen' | 'normal''overlay'CSS mix-blend-mode applied to the grain layer
monochromebooleantrueMonochrome
fpsnumber24Maximum redraws per second
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 { FilmGrainEngine, type FilmGrainOptions } from './film-grain.engine'

export interface FilmGrainProps extends Partial<FilmGrainOptions> {
  className?: string
  style?: CSSProperties
}

export const filmGrainDefaults: FilmGrainOptions = {
  intensity: 0.5,
  size: 1,
  speed: 1,
  blend: 'overlay',
  monochrome: true,
  fps: 24,
}

export function FilmGrain({
  intensity = filmGrainDefaults.intensity,
  size = filmGrainDefaults.size,
  speed = filmGrainDefaults.speed,
  blend = filmGrainDefaults.blend,
  monochrome = filmGrainDefaults.monochrome,
  fps = filmGrainDefaults.fps,
  className,
  style,
}: FilmGrainProps) {
  const engineRef = useRef<FilmGrainEngine | null>(null)
  if (!engineRef.current) engineRef.current = new FilmGrainEngine(filmGrainDefaults)
  const engine = engineRef.current
  const { containerRef, canvasRef, wake } = useDotCanvas(engine, { pointer: false, kind: 'webgl' })

  useEffect(() => {
    engine.setOptions({ intensity, size, speed, blend, monochrome, fps })
    wake()
  }, [engine, wake, intensity, size, speed, blend, monochrome, fps])

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