Quick start
Install ditherkit and render your first dithered image in five minutes.
ditherkit is split into three packages. You almost never install all three — pick the one that matches where you want the image processing to happen. Jump straight to install → if you already know which one you need.
Which package do I need?
Do you need to process images in the browser as users interact with
them — sliders, live previews, uploads?
→ @ditherkit/react. Client-side component backed by a
Web Worker so the UI stays responsive.
Are you on Next.js and want dithered images cached and served like
normal next/image output?
→ @ditherkit/next. Server-renders with Sharp, caches with
ISR, ships through next/image.
Are you writing a build script, a Cloudflare Worker, a Vue/Svelte app,
or anything that isn't React?
→ @ditherkit/core. Pure algorithm functions. No DOM, no
framework coupling.
Need more than one? Install the ones you need. They all share
@ditherkit/core under the hood, so there's no duplication.
@ditherkit/nextdoes not re-export@ditherkit/react. If you want interactive client-side dithering inside a Next.js app, install both packages — they live side-by-side. See Choose your package for the rationale.
Install
Pick the package for your use case.
@ditherkit/react
pnpm add @ditherkit/react
# or: npm install @ditherkit/react
# or: yarn add @ditherkit/reactPeer dependencies: react >= 19.2, react-dom >= 19.2.
@ditherkit/next
pnpm add @ditherkit/next sharpPeer dependencies: next >= 16.2, react >= 19.2, react-dom >= 19.2.
The sharp package is required at runtime for the server-side
image pipeline.
@ditherkit/core
pnpm add @ditherkit/coreZero runtime dependencies. Works in any JavaScript environment.
First working example
With @ditherkit/react
import { DitheredImage } from '@ditherkit/react'
export function Portrait() {
return (
<DitheredImage
src="/portrait.jpg"
alt="A portrait, dithered"
width={400}
height={600}
algorithm="Floyd-Steinberg"
/>
)
}The component fetches the image, processes it in a Web Worker, and
renders the result to a <canvas>. Passing width and height
pre-sizes the canvas so there's no layout shift when the image loads —
same contract as next/image.
With @ditherkit/next
Three files. No factory call, no shared lib module — withDitherkit
in your next.config writes a build-time env bridge that the
component and the route handler both read at runtime.
// next.config.ts — the ONE place ditherkit is configured
import type { NextConfig } from 'next'
import { withDitherkit } from '@ditherkit/next/config'
const config: NextConfig = {
// your existing config
}
export default withDitherkit(config, {
routeBase: '/api/dithered',
})// app/page.tsx
import { DitheredImageSSR } from '@ditherkit/next'
import portrait from '@/assets/portrait.jpg'
export default function Page() {
return (
<DitheredImageSSR
src={portrait}
alt="A portrait, dithered on the server"
algorithm="Atkinson"
priority
/>
)
}// app/api/dithered/[cacheKey]/[...params]/route.ts
export { GET } from '@ditherkit/next/route/app'For Pages Router, the route file is the only thing that changes:
// pages/api/dithered/[cacheKey]/[...params].ts
export { default } from '@ditherkit/next/route/pages'That's it — the same <DitheredImageSSR /> and the same
withDitherkit call work for both routers. See Route setup
for the full story, including how the singleton pattern keeps Sharp
out of your client bundle and the legacy createDitherkit factory
for multi-instance use cases. External http(s):// sources are off
by default — see External URLs to opt in.
Responsive images with <DitheredPicture />
Need different dithered variants at different viewport widths?
<DitheredPicture /> renders a <picture> element with zero
client JS — same withDitherkit config, same route handler:
import { DitheredPicture } from '@ditherkit/next'
import portrait from '@/assets/portrait.jpg'
<DitheredPicture
src={portrait}
alt="Responsive dithered portrait"
algorithm="Atkinson"
pixelSize={2}
variants={[
{ media: 1024, width: 600 },
{ media: 640, width: 400 },
{ media: 0, width: 240 },
]}
/>Or auto-generate variants from your layout function:
import { DitheredPicture, computeVariants } from '@ditherkit/next'
const variants = computeVariants({
containerWidth: (vw) => (Math.min(vw, 860) - 48) / 3,
maxOvershoot: 0.3,
pixelSize: 2,
})
<DitheredPicture src={portrait} variants={variants} />See <DitheredPicture /> for the
full reference — art direction, cover mode, string media queries,
load detection.
With @ditherkit/core
import {
floydSteinbergDither,
grayscale,
type Color,
} from '@ditherkit/core'
const palette: Color[] = [
{ r: 0, g: 0, b: 0 },
{ r: 255, g: 255, b: 255 },
]
// `pixels` is a Uint8ClampedArray of RGBA values,
// obtained from a canvas, Sharp, or anywhere else.
// grayscale first is optimal for this 2-colour palette; skip it
// for multi-colour palettes (Game Boy, PICO-8, etc).
grayscale(pixels)
floydSteinbergDither(pixels, width, height, palette)
// `pixels` is now dithered in place.No framework, no DOM. Call it from a build script, a Web Worker you own,
a Node CLI, a Cloudflare Worker, anywhere. See
@ditherkit/core for the full list of algorithms and
helpers.
Next steps
- Play with it. The playground lets you try every algorithm and control in real time.
- Pick a stack guide. Integrations has per-stack notes and links to complete example apps.
- Hit a snag? Troubleshooting covers the
common errors (CORS, missing
withDitherkit(), Sharp peer dep, etc.).