25
Componentregistry:component

Dithering Hover

Any image turns into a black-and-white Bayer dither under the cursor. The dithered zone follows the pointer with a ragged rim and radial streaks that break up and fade, rendered live on a canvas. Controls for zone size, dot size, matrix, inks, roughness and an inverted mode. Exports drawDither for static renders.

Installation

$ npx shadcn@latest add @creative-ui/dithering-hover

On an older shadcn CLI, use the URL: npx shadcn@latest add https://ui.krutikraut.com/r/dithering-hover.json

Preview

move the pointer over the image

Image
Mode
Zone size200px
Dot size4px
Matrix
Streaks28
Roughness0.60
Contrast1.1
Ink
Paper

Usage

Example.tsx
import { DitheringHover } from "@/components/dithering-hover"

export function Hero() {
  return (
    <DitheringHover
      src="https://images.unsplash.com/photo-1566054757965-8c4085344c96?w=900&q=80&auto=format&fit=crop"
      alt="Marble statue"
      zoneSize={260}
      dotSize={4}
      className="aspect-[4/5] w-80 rounded-xl"
    />
  )
}

Props

PropTypeDefaultDescription
src*string—Image URL. Cross-origin images must be served with CORS headers, or the canvas cannot read them.
altstring""Accessible description. Leave empty for decorative images.
zoneSizenumber240Diameter of the dithered zone in CSS pixels, 60 to 500.
dotSizenumber4Size of one dither pixel in CSS pixels, 1 to 16. 2 to 4 reads editorial, 8 to 16 reads graphic.
matrix2 | 4 | 88Bayer matrix order. 8 gives the smoothest tones, 2 the harshest.
darkstring"#000"Ink for dark pixels. Any CSS colour, including currentColor.
lightstring"#fff"Ink for light pixels. Pass "transparent" to let the photo show through the light pixels.
contrastnumber1Tone curve steepness, roughly 0.5 to 2.
brightnessnumber0Shift the tones lighter (positive) or darker (negative), -1 to 1.
streaksnumber28Streaks radiating from the zone. 0 gives a clean ragged circle.
streakLengthnumber1Streak reach as a multiple of the zone radius.
roughnessnumber0.6How ragged the rim and streaks are, 0 to 1.
mode"inside" | "outside""inside"inside dithers under the cursor. outside dithers the whole image and reveals the photo under the cursor.
easenumber0.2Pointer follow easing per frame, 0 to 1. 1 snaps, smaller values lag behind.
seednumber7Seed for the streak layout. Change it for a different pattern.
fit"cover" | "contain""cover"How the source fills the canvas.
crossOrigin"anonymous" | "use-credentials""anonymous"Pass undefined for same-origin images that must not send a CORS request.
onRendered() => void—Called once the image is loaded and first drawn.
childrenReact.ReactNode—Content layered over the canvas, such as captions or badges.

Also accepts every div prop, which is spread onto the root element.

Also exported

drawDither(canvas, image, width, height, options?)
The dither on its own, with no cursor and no React. Draws an image, canvas, video or ImageBitmap onto a canvas as a full-frame Bayer dither, so you can export a PNG with canvas.toBlob(). Takes dotSize, matrix, dark, light, contrast, brightness and fit.
ditherBitmap(image, width, height, options?)
Returns the raw 0/1 bitmap at one entry per dither pixel, plus its size, for your own renderers.

Notes

  • Size it like an image: give it a width with className. Without a height or aspect ratio it adopts the source image's aspect ratio once loaded.
  • The dither is computed once per size and option change and cached. Pointer moves only recomposite the patch around the cursor, so it stays smooth at large sizes.
  • The zone grows in when the pointer enters and shrinks out when it leaves. Works with touch and pen through pointer events; with reduced motion the zone snaps instead of easing.
  • Works best on high-contrast photography. Raise contrast for flat images.
  • The canvas gets role="img" and your alt as its label, or is hidden from assistive tech when alt is empty.

Dependencies

shadcn/utils