pnpm dlx shadcn@latest add @uigrove/liquid-smoke

Liquid Smoke

Slow plumes of smoke rising from the bottom of a section, drawn with warped noise on the GPU.

Installation
pnpm dlx shadcn@latest add @uigrove/liquid-smoke

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 { LiquidSmoke } from '@/components/uigrove/liquid-smoke'

<LiquidSmoke className="absolute inset-0" />
Props
NameTypeDefaultDescription
colorstring'#f5f5f7'Smoke color at full intensity
backgroundstring'#09090c'Background
intensitynumber0.7Intensity
scalenumber2.2Noise frequency, higher is finer
speednumber0.35Speed
heightnumber0.7How far up the plumes reach
warpnumber2.4Domain warping, more turbulence
grainnumber0.25Grain
seednumber3Seed
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 { LiquidSmokeEngine, type LiquidSmokeOptions } from './liquid-smoke.engine'

export interface LiquidSmokeProps extends Partial<LiquidSmokeOptions> {
  className?: string
  style?: CSSProperties
}

export const liquidSmokeDefaults: LiquidSmokeOptions = {
  color: '#f5f5f7',
  background: '#09090c',
  intensity: 0.7,
  scale: 2.2,
  speed: 0.35,
  height: 0.7,
  warp: 2.4,
  grain: 0.25,
  seed: 3,
}

export function LiquidSmoke({
  color = liquidSmokeDefaults.color,
  background = liquidSmokeDefaults.background,
  intensity = liquidSmokeDefaults.intensity,
  scale = liquidSmokeDefaults.scale,
  speed = liquidSmokeDefaults.speed,
  height = liquidSmokeDefaults.height,
  warp = liquidSmokeDefaults.warp,
  grain = liquidSmokeDefaults.grain,
  seed = liquidSmokeDefaults.seed,
  className,
  style,
}: LiquidSmokeProps) {
  const engineRef = useRef<LiquidSmokeEngine | null>(null)
  if (!engineRef.current) engineRef.current = new LiquidSmokeEngine(liquidSmokeDefaults)
  const engine = engineRef.current
  const { containerRef, canvasRef, wake } = useDotCanvas(engine, {
    pointer: false,
    kind: 'webgl',
    maxDpr: 1,
  })

  useEffect(() => {
    engine.setOptions({ color, background, intensity, scale, speed, height, warp, grain, seed })
    wake()
  }, [engine, wake, color, background, intensity, scale, speed, height, warp, grain, seed])

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