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-grainDependencies: 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
| Name | Type | Default | Description |
|---|---|---|---|
| intensity | number | 0.5 | Contrast of the grain against the surface |
| size | number | 1 | Grain cell size in CSS pixels |
| speed | number | 1 | How fast cells re-roll, 0 freezes the grain |
| blend | 'overlay' | 'soft-light' | 'screen' | 'normal' | 'overlay' | CSS mix-blend-mode applied to the grain layer |
| monochrome | boolean | true | Monochrome |
| fps | number | 24 | Maximum redraws per second |
| className | string | — | 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>
)
}
