# Halftone

A print halftone: round dots on a rotated screen that swell and shrink with a source, in one ink or up to three overlapping like CMYK.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/halftone.json](https://dithercn.dev/r/halftone.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/halftone.json
```

## Usage [#usage]

```tsx
import { Halftone } from "@/components/dithercn/halftone";

export function Hero() {
  return (
    <section className="relative isolate min-h-[480px]">
      <Halftone color="blue" className="absolute inset-0 -z-10" />
      <h1>Your headline</h1>
    </section>
  );
}
```

Each square of a rotated screen holds one round dot, sized by the source at its centre, the way a printing screen works. Dot edges are dithered at the screen's own device pixels, so dots stay round and grow smoothly from a speck to touching their neighbours. The screen slides slowly along its angle (`drift`), so dots swell and shrink as they cross the source.

Pass `channels` instead of `color` for two or three inks. Each gets its own screen, turned 30° from the last, and overlapping dots mix their colours. With blue, pink and orange you get the rosettes of a CMYK print.

## Examples [#examples]

### Spotlight behind a headline [#spotlight-behind-a-headline]

`source="radial"` grows the dots towards `center`. With the centre below the bottom edge, a `radius` under 1 and a transparent background, the dots swell up from the bottom of the section and run out before the text.

```tsx title="examples/halftone-spotlight.tsx"
import { Halftone } from "@/components/dithercn/halftone";

export default function HalftoneSpotlight() {
  return (
    <section className="relative isolate flex size-full flex-col items-center gap-3 px-6 pt-10 text-center sm:pt-14">
      <Halftone
        source="radial"
        center={[0.5, 1.2]}
        radius={0.62}
        color="orange"
        background="transparent"
        className="absolute inset-0 -z-10"
      />
      <h3 className="max-w-md text-3xl font-medium tracking-[-0.03em] text-balance">
        Printed overnight, sold out by noon
      </h3>
      <p className="text-muted-foreground max-w-sm text-sm text-pretty">
        Limited risograph prints, numbered and signed. The next run opens on
        Friday.
      </p>
    </section>
  );
}
```

### Duotone ramp in a card [#duotone-ramp-in-a-card]

`source="linear"` grows the dots towards `direction`. Two channels at a shallow 15° screen, held still, and faded in from the left with a CSS mask.

```tsx title="examples/halftone-card.tsx"
import { Halftone } from "@/components/dithercn/halftone";

export default function HalftoneCard() {
  return (
    <div className="bg-background relative isolate flex w-full max-w-xs [--dither-paper:var(--background)] flex-col gap-1 overflow-hidden rounded-2xl border p-5">
      <Halftone
        source="linear"
        direction={100}
        angle={15}
        size={8}
        channels={["blue", "purple"]}
        animate={false}
        className="absolute inset-y-0 right-0 -z-10 w-3/5 [mask-image:linear-gradient(to_right,transparent,#000_40%)]"
      />
      <span className="text-muted-foreground text-sm">Print runs</span>
      <span className="text-3xl font-semibold tracking-tight tabular-nums">
        3,912
      </span>
      <span className="text-muted-foreground mt-10 text-xs">
        Across 14 presses this month
      </span>
    </div>
  );
}
```

### Three-ink print [#three-ink-print]

A coarse 16px screen in three inks behind an editorial block.

```tsx title="examples/halftone-print.tsx"
import { Halftone } from "@/components/dithercn/halftone";

export default function HalftonePrint() {
  return (
    <section className="relative isolate flex size-full items-end overflow-hidden p-6 sm:p-10">
      <Halftone
        channels={["blue", "pink", "orange"]}
        size={16}
        angle={15}
        scale={0.7}
        className="absolute inset-0 -z-10"
      />
      <div className="bg-background/85 flex max-w-sm flex-col gap-2 rounded-xl p-5 backdrop-blur-sm">
        <span className="text-muted-foreground font-mono text-xs tracking-wide uppercase">
          Issue 07
        </span>
        <h3 className="text-2xl font-medium tracking-[-0.03em] text-balance">
          Three inks, one press, no plates to clean
        </h3>
        <p className="text-muted-foreground text-sm text-pretty">
          Cyan, magenta and yellow screens at 15°, 45° and 75°. Where the dots
          cross, the colours mix.
        </p>
      </div>
    </section>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `DitherColor` | `"ink"` | Colour of the dots: a palette name such as "blue", a CSS variable name, or any CSS colour. |
| `channels` | `[DitherColor, DitherColor, DitherColor?]` | `–` | Two or three inks on separate screens, 30° apart. Overlaps mix. Use instead of color. |
| `background` | `DitherColor` | `"--dither-paper"` | Colour behind the dots. "transparent" lays the screen over the content underneath. |
| `source` | `"noise" \| "radial" \| "linear"` | `"noise"` | What sizes the dots: soft drifting clouds, distance from a point, or a ramp. |
| `center` | `[x: number, y: number]` | `[0.5, 0.5]` | Radial only. Where the dots are largest, as fractions of the box from the top left. |
| `radius` | `number` | `1` | Radial only. Where the dots run out. 1 reaches the farthest corner. |
| `direction` | `number` | `0` | Linear only. Direction the dots grow towards, in degrees as in CSS. |
| `size` | `number` | `10` | Distance between dot centres in CSS pixels. |
| `angle` | `number` | `45` | Screen angle in degrees. Each extra channel adds 30°. |
| `intensity` | `number` | `0.9` | Size of the largest dot, 0 to 1. At 1 the biggest dots just touch. |
| `scale` | `number` | `1` | Noise only. Size of the clouds. |
| `drift` | `number` | `1` | How fast the screen slides along its angle. 0 holds the screen still. |
| `speed` | `number` | `1` | How fast the source moves. |
| `animate` | `boolean` | `true` | false stops the motion: a still frame if it starts off, the current frame if it was moving. |
| `cell` | `number` | `one device pixel` | Size of one rendered pixel in CSS pixels. Larger cells give blockier dots. |
| `className` | `string` | `–` | Size and position. The halftone fills this box. |

## Notes [#notes]

* The halftone is decorative and hidden from assistive technology. Put real text on top of it, not inside it.
* With reduced motion turned on, it draws one still frame and stops.
* It pauses when scrolled out of view or when the tab is hidden, and releases its WebGL context when far offscreen. Without WebGL it shows a CSS dot screen in the same inks.
* Overlap colours are mixed with CSS `color-mix` in oklch, so they follow the theme along with the inks.