# ASCII countdown

A launch countdown in big glyph digits that scramble as they change.

> 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 asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn 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 `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/ascii-countdown.json](https://asciicn.dev/r/ascii-countdown.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://asciicn.dev/r/ascii-countdown.json
```

## Usage [#usage]

```tsx
import { AsciiCountdown } from "@/components/asciicn/ascii-countdown";

export function Launch() {
  return (
    <AsciiCountdown
      to="2026-12-01T17:00:00Z"
      onComplete={() => console.log("launched")}
    />
  );
}
```

`to` takes a `Date`, an ISO string or epoch milliseconds. The clock shows days, hours, minutes and seconds, each digit drawn from a 5x7 pixel font. At zero it draws `doneText` in the same type and calls `onComplete` once.

The countdown fills its container's width. It picks the largest digit that fits, up to `size` pixels tall, by changing how many cells each font pixel covers. When one row would make the digits smaller than 40 px, as on a phone, it splits into two rows: days and hours, then minutes and seconds.

When a digit changes, only the cells that differ run through a few glyphs and settle, each at its own moment. `scramble` sets how long that takes. The colons dim and brighten once a second, in step with the seconds.

## Examples [#examples]

### Banner [#banner]

`size={22}` and 6 px cells keep the digits small enough for a strip at the top of a page. Labels and ghost pixels are off, and only the colons take the accent.

```tsx title="examples/ascii-countdown-banner.tsx"
"use client";

import { useState } from "react";

import { AsciiCountdown } from "@/components/asciicn/ascii-countdown";

const AHEAD = ((2 * 24 + 11) * 60 + 8) * 60_000 + 40_000;

export default function AsciiCountdownBanner() {
  const [launch] = useState(() => Date.now() + AHEAD);

  return (
    <div className="flex w-full max-w-3xl flex-wrap items-center justify-between gap-x-6 gap-y-3 border border-border px-4 py-3">
      <p className="font-mono text-muted-foreground text-xs uppercase tracking-[0.2em]">
        Public beta opens in
      </p>
      <div className="w-full max-w-72">
        <AsciiCountdown
          to={launch}
          size={22}
          cell={6}
          labels={false}
          accentOn="colons"
          ghost={false}
        />
      </div>
    </div>
  );
}
```

### Done state [#done-state]

Five seconds out, with `hideZeroDays`. At zero the last digits scramble into `doneText` and `onComplete` changes the button.

```tsx title="examples/ascii-countdown-done.tsx"
"use client";

import { useState } from "react";

import { AsciiCountdown } from "@/components/asciicn/ascii-countdown";

export default function AsciiCountdownDone() {
  // Five seconds out: watch the last ticks turn into the done text.
  const [launch, setLaunch] = useState(() => Date.now() + 5000);
  const [launched, setLaunched] = useState(false);

  return (
    <div className="flex w-full max-w-3xl flex-col items-center gap-4">
      <AsciiCountdown
        to={launch}
        hideZeroDays
        doneText="WE ARE LIVE"
        onComplete={() => setLaunched(true)}
      />
      <button
        type="button"
        className="font-mono text-muted-foreground text-xs uppercase tracking-[0.2em] underline-offset-4 hover:text-foreground hover:underline"
        onClick={() => {
          setLaunched(false);
          setLaunch(Date.now() + 5000);
        }}
      >
        {launched ? "Count again" : "Counting down"}
      </button>
    </div>
  );
}
```

### Braille dot matrix [#braille-dot-matrix]

`charset="braille"` draws each font pixel as a patch of dots. `colorMode="duotone"` shades each digit from the top of the `sodium` ramp down. Duotone ramps are tuned for dark paper.

```tsx title="examples/ascii-countdown-braille.tsx"
"use client";

import { useState } from "react";

import { AsciiCountdown } from "@/components/asciicn/ascii-countdown";

const AHEAD = ((14 * 24 + 6) * 60 + 52) * 60_000 + 3000;

export default function AsciiCountdownBraille() {
  const [launch] = useState(() => Date.now() + AHEAD);

  return (
    <div className="w-full max-w-4xl">
      <AsciiCountdown
        to={launch}
        charset="braille"
        colorMode="duotone"
        ramp="sodium"
        accentOn="colons"
      />
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `to` | `Date \| string \| number` |  | The moment to count down to: a Date, an ISO string or epoch milliseconds. |
| `labels` | `readonly [string, string, string, string] \| false` | `["DAYS", "HRS", "MIN", "SEC"]` | Labels under days, hours, minutes and seconds, or false for none. |
| `doneText` | `string` | `"LIVE"` | Drawn in the same big type once the countdown reaches zero. Letters, digits and ! ? . - /. |
| `onComplete` | `() => void` |  | Called once when the countdown reaches zero, or on mount if to has already passed. |
| `scramble` | `number` | `0.45` | Seconds a changed digit scrambles before it settles. 0 turns it off. |
| `hideZeroDays` | `boolean` | `false` | Drop the days group once fewer than 24 hours are left. |
| `charset` | `CharsetName` | `"blocks"` | Glyphs the digits are drawn with. blocks draws half blocks, quadrants and braille split each cell, ramps use their densest glyph. |
| `chars` | `string` |  | A custom ramp from empty to dense. Its densest glyph inks the digits. Wins over charset. |
| `colorMode` | `"mono" \| "duotone"` | `"mono"` | mono draws digits in ink and the accent; duotone shades the ink digits top to bottom along ramp. |
| `ramp` | `RampName \| readonly string[]` | `"sodium"` | duotone: a ramp name or #rrggbb stops. |
| `accent` | `AsciiColor` | `"orange"` | The accent colour: a palette name, CSS variable name or any CSS colour. |
| `accentOn` | `"seconds" \| "colons" \| "both" \| "none"` | `"seconds"` | Which parts take the accent. |
| `ghost` | `boolean` | `true` | Faint dots on the unlit pixels of each digit. |
| `size` | `number` | `128` | Largest digit height in CSS pixels. The digits shrink to fit the width. |
| `cell` | `number` | `8` | Glyph cell height in CSS pixels. Smaller cells fit the width more closely. |
| `animate` | `boolean` | `true` | false holds still: no scramble or blinking, though the time still ticks. |
| `fps` | `number` | `30` | Frame rate cap. |
| `paper` | `AsciiColor \| "transparent"` | `"transparent"` | Background behind the glyphs. |
| `fx` | `PostFx` |  | CRT effects, drawn as a CSS overlay. |
| `handle` | `RefObject<AsciiHandle \| null>` |  | Filled with a handle for export: the latest frame and the canvas. |

Other props pass through to the root `div`.

## Notes [#notes]

* Changed digits scramble for a beat and settle, the colons blink softly, each group has a label under it, and at zero it shows `doneText` and calls `onComplete`.
* The digits are drawn on a canvas that is `aria-hidden`. A visually hidden `role="timer"` element gives the time left to the minute and the target date, so a screen reader is not told every second.
* Nothing that depends on the clock renders on the server. The first client render matches the server, then the countdown starts, so there is no hydration mismatch.
* With reduced motion, or `animate={false}`, digits change without scrambling and the colons hold steady. The time still updates every second.
* The canvas pauses offscreen and in hidden tabs. When it comes back, the digits that changed while it was away scramble once.
* Labels are page text under the canvas, so they stay readable at any cell size. They take `--ascii-ink` at reduced opacity and are hidden from screen readers, which get the timer text instead.