Skip to content
Founding Lifetime: every Pro skillfirst 100 members · $169 onceUpgrade
Lookbk
Sign In
Effect guide

Dither effect: 1-bit dots from smooth tones

Gradients and photos rebuilt from two colors of square dots, like an old Mac screen or a thermal print. What dithering is, ordered versus error diffusion, a canvas starter that shows both side by side, and skills that build a finished dithered hero or deck.

Free skills to start. Pro is $79 a year for every skill, or $9.99 a month for 10. See pricing

What is a dither effect?

Dithering trades color depth for pattern. When a picture has to be shown with fewer colors than it holds, often just black and white, each pixel is rounded to the nearest color it is allowed, and the rounding is spread around as a fine pattern of dots. Up close you see the dots; from a step back your eye averages them into the gray that was there.

There are two families. Ordered dithering compares every pixel with a fixed grid of thresholds, the Bayer matrix, and leaves a regular crosshatch texture. Error diffusion, the best known being Floyd–Steinberg, rounds one pixel at a time and pushes the leftover error onto the neighbors it has not reached yet, which gives an organic, grainy look with finer detail. Limited palettes work the same way: two inks, four greens or sixteen colors, each pixel picks one.

It started as a necessity for early screens, printers and GIFs. On the web today it is a style: the 1-bit look of retro computers, e-ink and handheld games, used on heroes, product renders, posters and slides because it gives a picture texture and a clear, technical character with almost no color.

How the effect works

Ordered dither is one comparison per pixel. Take the pixel brightness from 0 to 1, look up the threshold for its position in an 8 × 8 Bayer matrix ((value + 0.5) / 64, using x mod 8 and y mod 8), and ink it light if it is brighter than the threshold. Each pixel is independent, so it runs as easily in a fragment shader on gl_FragCoord as in a canvas loop, and the pattern stays put from frame to frame.

Floyd–Steinberg walks the image row by row. Round the pixel to 0 or 1, take the difference between what it was and what it became, and add 7/16 of that error to the pixel on the right, 3/16 below-left, 5/16 below and 1/16 below-right. Because each pixel depends on the ones before it, it is a CPU job, and a small change anywhere ripples through everything after it.

  1. 01

    Work at low resolution

    Draw the picture into a small buffer, a few hundred pixels wide, so each dither dot can be several screen pixels.

  2. 02

    Read the tone

    Turn each pixel into luminance from 0 to 1. Convert from sRGB to linear first if the average brightness has to match the original.

  3. 03

    Threshold or diffuse

    Compare with the Bayer cell for a stable, patterned look, or round and pass the error on for a finer, grainier one.

  4. 04

    Scale up by whole pixels

    Enlarge by an integer factor with image-rendering: pixelated, so every dot stays a crisp square.

HTML

Ordered and Floyd–Steinberg dither on a canvas

Dithers a generated gradient and a lit ball in two inks: the left half with an 8 × 8 Bayer matrix, the right half with Floyd–Steinberg, so you can compare them as the ball rolls across. It scales by whole device pixels and holds one frame for reduced motion.

<canvas id="dither" style="display:block;margin:auto;image-rendering:pixelated"></canvas>
<script>
const W = 240, H = 135 // the dither works on these pixels; CSS only enlarges them
const canvas = document.getElementById('dither')
canvas.width = W; canvas.height = H
const g = canvas.getContext('2d')
const img = g.createImageData(W, H)
const INK = [20, 20, 24], PAPER = [238, 232, 218]
const still = matchMedia('(prefers-reduced-motion: reduce)').matches
// 8x8 Bayer matrix, built by repeating the 2x2 pattern inside itself
let M = [[0, 2], [3, 1]]
while (M.length < 8) {
  const n = M.length
  M = Array.from({ length: 2 * n }, (_, y) => Array.from({ length: 2 * n },
    (_, x) => 4 * M[y % n][x % n] + [[0, 2], [3, 1]][(y / n) | 0][(x / n) | 0]))
}
// The picture to dither: a soft gradient with a lit ball rolling across it, 0 dark to 1 light
function tone(x, y, t) {
  const cx = W * (0.5 + 0.3 * Math.sin(t * 0.4)), cy = H * 0.52, r = H * 0.34
  const dx = (x - cx) / r, dy = (y - cy) / r, d2 = dx * dx + dy * dy
  if (d2 < 1) return Math.max(0.04, -0.55 * dx - 0.5 * dy + 0.65 * Math.sqrt(1 - d2))
  return 0.12 + 0.62 * (x / W) * (1 - 0.4 * y / H)
}
// Whole device pixels per dot, or the pattern smears into moire
function fit() {
  const k = Math.max(1, Math.floor(Math.min(innerWidth, 960) * devicePixelRatio / W))
  canvas.style.width = W * k / devicePixelRatio + 'px'
  canvas.style.height = H * k / devicePixelRatio + 'px'
}
fit(); addEventListener('resize', fit)
const v = new Float32Array(W * H), half = W >> 1
function frame(ms) {
  const t = still ? 0 : ms / 1000
  for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) v[y * W + x] = tone(x, y, t)
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      const i = y * W + x
      let on
      if (x < half) {
        on = v[i] > (M[y & 7][x & 7] + 0.5) / 64 // left: ordered, a fixed threshold per pixel
      } else {
        on = v[i] > 0.5 // right: Floyd-Steinberg, round and pass the error on
        const e = v[i] - (on ? 1 : 0)
        if (x + 1 < W) v[i + 1] += e * 7 / 16
        if (y + 1 < H) {
          if (x > half) v[i + W - 1] += e * 3 / 16
          v[i + W] += e * 5 / 16
          if (x + 1 < W) v[i + W + 1] += e / 16
        }
      }
      const c = on ? PAPER : INK, p = i * 4
      img.data[p] = c[0]; img.data[p + 1] = c[1]; img.data[p + 2] = c[2]; img.data[p + 3] = 255
    }
  }
  g.putImageData(img, 0, 0)
  if (!still) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
</script>

A starting point written for this page, free to use in your own projects. The skills below build the finished version with checks, fallbacks and a recorded video.

What to get right on a real page

  1. 01

    Scale by whole pixels only

    A dot pattern resized by 1.5 × turns into moiré and blur. Render small, enlarge by 2 ×, 3 × or 4 × in device pixels, and set image-rendering: pixelated so the browser does not smooth it.

  2. 02

    Keep text out of the dots

    Small type dithered into a pattern loses its edges and its contrast. Keep body copy as real text on a solid ink or paper area, and dither only large display type, if any.

  3. 03

    Use ordered dither for motion

    Error diffusion recomputes the whole pattern every frame, so moving or fading pictures crawl and shimmer. A Bayer or blue-noise threshold stays in place and only the changing areas flip.

  4. 04

    Expect heavy video files

    Dither is high-frequency noise, which is what video compression handles worst. Record at the dot size you show, and check the MP4 at the bitrate you will post at.

  5. 05

    Hold a still frame for reduced motion

    Respect prefers-reduced-motion: show one finished frame instead of the loop. A visitor who turned motion off still gets the look.

Skills that build a dither effect

A hero that dithers everything in it, a launch deck that dithers your renders, and a hero that draws a 3D object or your picture in ordered dither dots.

  1. Dither Laser: an example of what it makes
    web
    Dither LaserClaude CodeCodexCursor+3 more agents

    Motion Heroes · Pro

    Dither Laser

    WebGL dither and laser hero · page + embed · 1080p loop

    A wall of 1-bit phosphor swept by four lasers from the corners. Smoke, beam haze, room light, the afterglow and the headline all pass through one 8 × 8 Bayer dither at whole device pixels, each dot in a single pure ink, and the letters the beams cross spark and fade back in two stages. A light version runs on photochromic paper, and the video loops every 12 seconds.

    Worth knowing: Type-effect headlines are capped at 8 characters a line, and without WebGL visitors get a still picture.

    Needs: Node.js 18+, Chrome (or Chromium/Edge), and ffmpeg for the video

    See what Dither Laser makes
  2. Dither Deck: an example of what it makes
    slides
    Dither DeckClaude CodeCodexCursor+3 more agents

    Decks · Pro

    Dither Deck

    Dark 1-bit HTML deck with scan-line reveals + PDF + editable PPTX · 9 layouts, 7 to 12 slides

    A dark launch keynote where your renders and photos become ordered Bayer dither in your ink color. A laser line sweeps each slide in and pictures resolve from coarse dots to fine; the dither is drawn once at build time, so the browser deck, the PDF and the PowerPoint file show the same dots.

    Worth knowing: Every picture becomes dots, and the ground stays near-black.

    Needs: Node.js 18+ and Chrome for the dither, checks and exports, a render or photo or two; pptxgenjs only for the PowerPoint file

    See what Dither Deck makes
  3. ASCII Signal Hero: an example of what it makes
    web
    ASCII Signal HeroClaude CodeCodexCursor+3 more agents

    Motion Heroes · Pro

    ASCII Signal Hero

    WebGL2 glyph hero · page + embed · 1080p MP4

    A 3D object built in code, or your own picture, rendered live beside a headline that types itself on. Its dither set draws square dots in an ordered 4 × 4 dither instead of glyphs, in two to four colors from your palette.

    Worth knowing: The default set is ASCII glyphs; choose the dither set. The live effect needs WebGL2.

    Needs: Node.js 18+, Chrome (or Chromium/Edge) and ffmpeg; visitors need WebGL2 for the live effect

    See what ASCII Signal Hero makes

Install

Build a dither effect with your agent

Each skill is a SKILL.md folder, so it installs the same way in Claude Code, Codex, Cursor and other agents. The example below uses Dither Laser; each skill page has its own prompt.

1. Paste the install prompt

Your agent runs the installer. The first time, it shows a sign-in link so you can approve it in the browser.

Install the "Dither Laser" skill from Lookbk (https://lookbk.dev/skills/dither-laser-hero).
1. Run: curl -fsSL https://lookbk.dev/cli.js | node - add dither-laser-hero
2. If it prints a sign-in link, show it to me. When I say I approved it, run the same command again.
3. When the install finishes, read the SKILL.md it installed and tell me what you need from me to make the first result.

Or run it in a terminal

curl -fsSL https://lookbk.dev/cli.js | node - add dither-laser-hero

2. Start a new session

Agents load skills when a session starts. The installer writes the folder where your agent looks:

Claude Code
~/.claude/skills/dither-laser-hero
Codex
~/.agents/skills/dither-laser-hero
Cursor
~/.agents/skills/dither-laser-hero

3. Ask for the result

Describe it in plain words, for example:

Use the dither-laser-hero skill to build a hero for our late-night club: our 2-word headline printed into a 1-bit phosphor wall that 4 violet lasers write on, our lede and one call to action, plus a 12-second 1080p loop.

Another agent? Install a skill in any agent

FAQ

Questions

Still stuck? The guides cover installing, agents and licensing in more detail.

Read the guides
What is the dither effect?

A way to show tones with fewer colors than the picture has: each pixel is rounded to an allowed color and the error is spread out as a pattern of dots, which the eye blends back into shades.

What is the difference between ordered dithering and Floyd–Steinberg?

Ordered dithering compares each pixel with a fixed threshold grid, giving a regular crosshatch that stays still in animation. Floyd–Steinberg passes each pixel’s rounding error to its neighbors, giving finer, grainier detail that shimmers when the picture moves.

Can I dither an image with CSS?

Not directly: CSS cannot threshold pixels. An SVG filter can approximate ordered dither by adding a tiled threshold pattern and snapping the result to two levels, but a canvas or a shader, as in the starter, is simpler and exact.

What is the difference between dithering and halftone?

Halftone varies the size of dots on a regular grid. Dithering keeps every dot the same size, one pixel, and varies how many are inked and where.

Why does my dither look blurry or striped?

It is being scaled by a fraction of a pixel. Show it at an integer scale in device pixels with image-rendering: pixelated, and avoid CSS widths like 100% on the canvas.