# Dissolve text

Text that dissolves in through Bayer thresholds by character, word or line, in any order, settling solid or into a dithered texture, and can dissolve back out.

> 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/dissolve-text.json](https://dithercn.dev/r/dissolve-text.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/dissolve-text.json
```

## Usage [#usage]

```tsx
import { DissolveText } from "@/components/dithercn/dissolve-text";

export function Headline() {
  return (
    <DissolveText as="h2" accent="blue" className="text-6xl font-semibold">
      Arrives one dot at a time
    </DissolveText>
  );
}
```

Each character is masked by a 4×4 Bayer matrix that fills in level by level, from no dots to solid, so the letters build up out of the dither. By default it plays once when the text scrolls into view, one character after another. Use the replay button on the preview to see it again.

`direction` sets the order: `start` (reading order), `end`, `center` (from the middle out), `edges` (from both ends in) or `random`, a shuffle that is the same on every visit. `accent` gives each unit a colour while it arrives, fading to the text colour over twice the `duration`.

`fill` sets what the letters settle into. `solid` ends as plain text. `gradient` stops at a dither level (`level`, 0.625 by default). `dotted` and `hatched` end in the style kit's textures, which suit large, heavy type.

## Examples [#examples]

### Hatched poster type [#hatched-poster-type]

`fill="hatched"` and `direction="center"` on a heavy display face, with the line below following from the middle out.

```tsx title="examples/dissolve-text-hatched.tsx"
import { DissolveText } from "@/components/dithercn/dissolve-text";

export default function DissolveTextHatched() {
  return (
    <section className="flex flex-col items-center gap-2 px-6 text-center">
      <DissolveText
        as="h3"
        direction="center"
        color="purple"
        fill="hatched"
        cell={3}
        duration={800}
        className="text-6xl leading-none font-black tracking-[-0.06em] sm:text-8xl"
      >
        Afterglow
      </DissolveText>
      <DissolveText
        direction="center"
        delay={500}
        className="text-muted-foreground font-mono text-xs tracking-[0.18em] uppercase"
      >
        Live set · Friday 22:00
      </DissolveText>
    </section>
  );
}
```

### Word by word [#word-by-word]

`by="word"` reveals whole words, which reads better for a paragraph. An 8×8 matrix with 2px cells gives a finer grain at body sizes, and the heading arrives in green.

```tsx title="examples/dissolve-text-words.tsx"
import { DissolveText } from "@/components/dithercn/dissolve-text";

export default function DissolveTextWords() {
  return (
    <div className="flex max-w-md flex-col gap-4 px-6">
      <DissolveText
        as="h3"
        by="word"
        matrix="bayer8"
        cell={2}
        accent="green"
        className="text-2xl font-semibold tracking-[-0.03em]"
      >
        Quarterly report
      </DissolveText>
      <DissolveText
        as="p"
        by="word"
        matrix="bayer8"
        cell={2}
        delay={200}
        stagger={45}
        className="text-muted-foreground text-base text-pretty"
      >
        Revenue grew in every region this quarter. Most of it came from teams
        that moved their dashboards to live data in the spring.
      </DissolveText>
    </div>
  );
}
```

### Dissolving out and in [#dissolving-out-and-in]

Set `show={false}` and the text dissolves back out. Here a timer hides the word, swaps it, and shows the next one in a random order. Each letter arrives white and settles into the ink colour.

```tsx title="examples/dissolve-text-cycle.tsx"
"use client";

import { useEffect, useState } from "react";

import { DissolveText } from "@/components/dithercn/dissolve-text";

const WORDS = ["dashboards", "landing pages", "launch posts", "status pages"];
const HOLD = 2600;
const EXIT = 520;

export default function DissolveTextCycle() {
  const [index, setIndex] = useState(0);
  const [show, setShow] = useState(true);

  useEffect(() => {
    const timer = setTimeout(
      () => {
        if (show) {
          setShow(false);
        } else {
          setIndex((value) => (value + 1) % WORDS.length);
          setShow(true);
        }
      },
      show ? HOLD : EXIT
    );
    return () => clearTimeout(timer);
  }, [show]);

  return (
    <p className="px-6 text-center text-3xl leading-tight font-semibold tracking-[-0.04em] sm:text-5xl">
      <span className="text-muted-foreground">Dithers for</span>
      <br />
      <DissolveText
        key={index}
        show={show}
        trigger="mount"
        duration={440}
        stagger={22}
        direction="random"
        color="ink"
        accent="--foreground"
      >
        {WORDS[index] ?? ""}
      </DissolveText>
    </p>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `string` |  | The text. |
| `as` | `"h1" \| "h2" \| "h3" \| "h4" \| "p" \| "span" \| "div"` | `"span"` | The element to render. |
| `by` | `"char" \| "word" \| "line"` | `"char"` | Dissolve each character, each word, or the whole text at once. |
| `direction` | `"start" \| "end" \| "center" \| "edges" \| "random"` | `"start"` | The order units dissolve in. |
| `trigger` | `"mount" \| "in-view"` | `"in-view"` | Play when mounted, or when the text first scrolls into view. |
| `show` | `boolean` | `true` | false dissolves the text out. true brings it back. |
| `color` | `DitherColor` | `the text colour` | Text colour: a palette name or any CSS colour. |
| `accent` | `DitherColor \| "none"` | `"none"` | Colour each unit has while it arrives, fading to the text colour. |
| `fill` | `"solid" \| "gradient" \| "dotted" \| "hatched"` | `"solid"` | What the letters settle into. |
| `level` | `number` | `0.625` | Gradient only: the coverage the letters settle at, 0 to 1. |
| `duration` | `number` | `600` | Milliseconds for one character, word or line. |
| `stagger` | `number` | `28 per character, 90 per word` | Milliseconds between one unit and the next. |
| `delay` | `number` | `0` | Milliseconds before the first unit starts. |
| `speed` | `number` | `1` | Multiplies every timing. 2 plays twice as fast. |
| `matrix` | `"bayer2" \| "bayer4" \| "bayer8"` | `"bayer4"` | The threshold pattern the letters fill in through. |
| `cell` | `number` | `--dither-cell` | Size of one dither cell in CSS pixels. |

## Notes [#notes]

* The text stays real text, one inline element per character or word, so it can be selected, copied and indexed. While it is dissolved out, it is hidden from assistive technology.
* The animation is plain CSS keyframes that step a `mask-image` through the matrix's levels with an ease-out timing. A text that plays on mount needs no JavaScript to run.
* With reduced motion turned on, the text appears and disappears instantly. A textured `fill` still applies.
* Text waiting for `in-view` stays hidden until it scrolls into view. Without JavaScript it is shown as normal text.