ditherkit

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/next does 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/react

Peer dependencies: react >= 19.2, react-dom >= 19.2.

@ditherkit/next

pnpm add @ditherkit/next sharp

Peer 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/core

Zero 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.).

On this page