Dither Image
Dither any image at a chosen bit depth with Bayer, Atkinson, Floyd–Steinberg or plain threshold quantisation.
Installation
npx smoothui-cli add dither-imageFeatures
- Four kernels — ordered
bayer8×8, error-diffusingatkinsonandfloyd-steinberg, and a hardthreshold levelsfrom2to8builds the tone ramp, so you get one-bit, tri-tone or eight-step results from the same sourcepaletteaccepts any list of CSS colours and is resampled acrosslevels; leave it out and the ramp is derived from the--color-foregroundand--color-backgroundtheme tokens, so it follows light and dark mode- Canvas 2D pipeline: the image is drawn downscaled to the
pixelSizegrid, quantised overImageData, then upscaled withimageSmoothingEnabled = falsefor crisp blocks devicePixelRatioaware backing store, capped at 2× so large images stay cheapprogressivewipes the dithered pass in when the image enters the viewport, driven by anIntersectionObserverthat disconnects after the first hit- Graceful degradation: a cross-origin failure, a decode error or an unavailable 2D context leaves the plain
<img>visible
Usage notes
- The source must be served with permissive CORS headers. The component requests it with
crossOrigin="anonymous"; if the canvas is tainted thegetImageDatacall throws, the component switches tofallbackand the untouched image stays on screen. - Error diffusion runs on luminance, so
palettebehaves as a tonal ramp ordered dark to light rather than as an arbitrary colour set. - Larger
pixelSizevalues mean a smaller grid and a cheaper pass —3or4is a good default for hero imagery.
Accessibility
ARIA Attributes
| Attribute | Element | Purpose |
|---|---|---|
aria-hidden="true" | Dither <canvas> | Marks the rendered canvas as decorative so the result is never announced twice |
alt | Underlying <img> | Carries the accessible name for the image; required by the API |
Screen Reader
- The real
<img>element stays in the DOM underneath the canvas and carries youralt, so assistive technology gets a genuine image node with a proper accessible name instead of an unlabeled canvas. - The canvas sits on top purely as presentation and is
aria-hidden, which also means the image is never announced twice. - Because the
<img>is a real element rather than a background, it is also the visual fallback when dithering cannot run — no blank space and no broken layout. - Nothing in the component is focusable, and no positive
tabIndexvalues are used.
Reduced Motion
This component respects the prefers-reduced-motion media query via useReducedMotion from Motion. When reduced motion is preferred, progressive is bypassed entirely: the IntersectionObserver is never created, the final dithered pass is shown immediately, and the reveal transition falls back to duration: 0. The dithering itself is a one-shot pass with no requestAnimationFrame loop, so nothing keeps running after the image is drawn.
Props
Created by
Powered by
© 2026 SmoothUI. Built by Eduardo Calvo.