Installation
pnpm dlx shadcn@latest add https://asciicn.dev/r/ascii-reveal.jsonUsage
import { AsciiReveal } from "@/components/asciicn/ascii-reveal";
export function Hero() {
return (
<AsciiReveal
src="/photos/launch.jpg"
alt="The rocket on the pad at dawn"
direction="down"
className="w-full max-w-xl"
/>
);
}AsciiReveal starts as the picture drawn in glyphs. It samples the photo with the same area-averaging sampler as AsciiImage, so every glyph is the average of the pixels under its cell.
When the reveal runs, an edge sweeps across the picture. Cells at the edge flicker through glyphs in the accent colour, then lock in as the photo. Behind the edge the real photo shows. Ahead of it the glyphs hold. The edge bends a little and has some grain, so cells hand over one by one instead of in straight lines.
trigger sets what starts it:
viewreveals once, when the image scrolls into view.hoverreveals on hover or keyboard focus, and goes back to glyphs on leave. On a touch screen a tap toggles it.manualfollows therevealedprop, so you can tie it to your own state.
The box keeps the image's aspect ratio. Pass ratio to reserve the space before the image loads, or set your own aspect-* class.
Examples
Hover cards
Each card reveals on hover or focus and sweeps back on leave. A short duration keeps them quick under the pointer.
Full-width radial reveal
direction="radial" opens the photo from the centre out. This one is Braille in truecolor and reveals once, when it scrolls into view. Press Replay to see it again.
Props
| Prop | Type | Default |
|---|---|---|
| src Image URL. Files on another origin need CORS headers, or the canvas can't read their pixels. | Type: string | Default – |
| alt What the image shows, passed to the <img>. Pass "" for a decorative image. | Type: string | Default – |
| trigger view reveals once on scroll into view. hover reveals on hover or focus. manual follows revealed. | Type: "view" | "hover" | "manual" | Default "view" |
| revealed With manual: true shows the photo, false the glyphs. | Type: boolean | Default false |
| direction Which way the edge travels. radial opens from the centre. random dissolves in patches. | Type: "down" | "up" | "left" | "right" | "radial" | "random" | Default "down" |
| duration Length of one full sweep in milliseconds. A reveal that turns back halfway takes half as long. | Type: number | Default 2400 |
| accent Colour of the glyphs at the edge. A palette name, a CSS variable or any CSS colour. | Type: AsciiColor | Default "orange" |
| ratio Width over height of the box. | Type: number | Default the image's own, 4/3 before it loads |
| charset Which glyphs draw the picture. | Type: "standard" | "dense" | "blocks" | "quadrants" | "braille" | "cp437" | "petscii" | "katakana" | "dots" | "edge" | Default "standard" |
| chars A custom ramp from empty to dense, such as " .oO@". Wins over charset. | Type: string | Default – |
| colorMode mono draws in one ink. truecolor takes each cell's colour from the photo. duotone reads tones through ramp. | Type: "mono" | "truecolor" | "ansi16" | "duotone" | "palette" | Default "mono" |
| phosphor With mono: a CRT ink and its dark glass. | Type: "p1" | "p3" | "ibm" | "blueprint" | Default – |
| ramp With duotone: a named ramp or your own stops, dark to light. | Type: RampName | string[] | Default "mainframe" |
| ansi With ansi16: which 16-colour table. | Type: "vga" | "xterm" | Default "vga" |
| grade Tone and colour grading before glyphs are chosen. | Type: { brightness?, contrast?, gamma?, saturation? } | Default – |
| invert true: dark pixels draw dense glyphs. false: bright pixels do. Unset, it follows the page. | Type: boolean | Default follows the theme |
| threshold Braille and quadrants: the tone that lights a dot. | Type: number | Default 0.5 |
| edgeThreshold With the edge charset: how strong a contour must be to draw a stroke. | Type: number | Default 0.22 |
| dither Ordered dithering between glyph levels and dots. | Type: "none" | "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "blue-noise" for Braille and quadrants, else "none" |
| background transparent draws glyphs only, over whatever is behind. | Type: "paper" | "transparent" | Default "paper" |
| fx CRT effects over the glyphs: scanlines, bloom, vignette. | Type: PostFx | Default – |
| cell Glyph cell height in CSS pixels. | Type: number | Default 9 |
| animate false swaps between glyphs and photo without a sweep, as reduced motion does. | Type: boolean | Default true |
| crossOrigin Passed to the image requests. | Type: "anonymous" | "use-credentials" | Default "anonymous" |
| handle Filled with a handle for export: the glyph layer as text, ANSI, SVG or PNG. | Type: RefObject<AsciiHandle | null> | Default – |
Notes
- The real
<img>is always in the page and carriesalt, so screen readers read the image in either state. The glyph layers arearia-hidden. - The image loads lazily. The glyphs sample it once the box comes within about 300 px of the viewport, and the
<img>usesloading="lazy". - With reduced motion, or
animate={false}, there is no sweep. Aviewreveal shows the photo straight away. Ahoverreveal shows the glyphs until you hover or focus it, then swaps to the photo. Amanualreveal shows whateverrevealedsays. - The sweep is the only motion. Before it starts and after it ends the component draws nothing new until a prop or the box size changes. Offscreen and in a hidden tab the sweep pauses where it is.
- During the sweep the photo is drawn through a mask of one pixel per cell, so the handover lines up with the glyph grid. At rest the
<img>itself shows. - With
trigger="hover"the box is focusable. If it sits inside a link or a button, usetrigger="manual"and driverevealedfrom that element's hover and focus instead. - The sample photos are public domain, from NASA, the Hubble Heritage Team and Ansel Adams for the US National Park Service, all from Wikimedia Commons.