23rd logo23rd

Dither Scan

An image that starts as a 1-bit dither and comes clean over a set number of diagonal scan passes.

Installation

pnpm dlx shadcn@latest add @23rd/dither-scan

Or from the public GitHub registry:

pnpm dlx shadcn@latest add radiumcoders/23rd.dev/dither-scan

Usage

The frame takes the image's aspect ratio and the full width of its parent. Size it with className.

"use client"

import { DitherScan } from "@/components/ui/dither-scan"

export function Cover() {
  return (
    <DitherScan
      src="/cover.jpg"
      alt="The studio at dusk"
      passes={5}
      className="mx-auto max-w-2xl rounded-xl"
    />
  )
}

Each pass shrinks the dither cells and adds tones and color, so more passes make a slower, finer reveal. With alternate, every other sweep runs back the way it came, like a scanner head. A short blur follows the line while each pass settles; set blur={0} to turn it off.

Start it yourself instead of on scroll:

"use client"

import { useState } from "react"

import { DitherScan } from "@/components/ui/dither-scan"

export function Scan() {
  const [key, setKey] = useState(0)

  return (
    <div className="flex flex-col items-center gap-4">
      <DitherScan
        src="/scan.jpg"
        autoPlay={false}
        replayKey={key}
        onComplete={() => console.log("clean")}
      />
      <button type="button" onClick={() => setKey((n) => n + 1)}>
        Scan
      </button>
    </div>
  )
}

Remote images must send CORS headers, because the dither reads their pixels. An image that can't be read is shown as is, without the scan.

Props

PropTypeDefault
srcstringrequired
altstring""
passesnumber4, 1–12
durationnumber1400, ms per pass
pausenumber280, ms between passes
anglenumber45, degrees of travel
alternatebooleantrue
pixelSizenumber6, first-pass cell in px
blurnumber6, px, 0 is off
colors[shadow: string, highlight: string]theme ink and paper
lineColorstringinverts what's under it
fit"cover" | "contain""cover"
autoPlaybooleantrue, when in view
loopbooleanfalse
loopDelaynumber2400, ms on clean image
replayKeynumber0
theme"light" | "dark" | "auto""auto"
onStart() => voidnone
onPass(pass: number, passes: number) => voidnone
onComplete() => voidnone
classNamestringnone

With reduced motion the image is shown clean, without sweeps.