Images
Any photo as live ASCII. Dither, colour from the source, reveal the original on hover.

Controls
pnpm dlx shadcn@latest add @asciicn/ascii-imagePulls in the engine at lib/ascii on first install. The @asciicn namespace needs one line in components.json, see Installation.
Install the engine first with npx shadcn@latest add @asciicn/ascii-core, or copy lib/ascii from the repository.
Copy this into components/ascii/ascii-image.tsx:
"use client"
import { useEffect, useState } from "react"
import { AsciiCanvas, type AsciiCanvasProps } from "@/lib/ascii"
import { cn } from "@/lib/utils"
export type AsciiImageProps = Omit<AsciiCanvasProps, "source"> & {
src: string
alt: string
/** Cross-fade to the original photo on hover. */
reveal?: boolean
}
export function AsciiImage({ src, alt, reveal = false, className, ...props }: AsciiImageProps) {
const [image, setImage] = useState<HTMLImageElement | null>(null)
useEffect(() => {
const img = new Image()
img.crossOrigin = "anonymous"
img.onload = () => setImage(img)
img.src = src
return () => {
img.onload = null
}
}, [src])
return (
<div
className={cn("group relative w-full overflow-hidden", className)}
style={{ aspectRatio: image ? `${image.naturalWidth} / ${image.naturalHeight}` : "1" }}
>
<AsciiCanvas source={image} aria-label={alt} className="absolute inset-0" {...props} />
{reveal && (
// eslint-disable-next-line @next/next/no-img-element -- crossfade target must be the same file the canvas samples
<img
src={src}
alt=""
className="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 group-hover:opacity-100"
/>
)}
</div>
)
}
import { AsciiImage } from "@/components/ascii/ascii-image"
<AsciiImage
src="/team/ada.jpg"
alt="Ada Lovelace"
dither="floyd"
fontSize={7}
reveal
/>edges traces contours with | / - \ where the tone changes sharply, and a palette maps tone to colour.
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | — | Image URL. Cross-origin images need CORS headers. |
| alt | string | — | Accessible label for the canvas. |
| reveal | boolean | false | Cross-fade to the original on hover. |
| Prop | Type | Default | Description |
|---|---|---|---|
| charset | CharsetName | string | "standard" | Named ramp or your own string, empty to dense. "braille" packs 2×4 dots per cell, "quadrants" 2×2 blocks. |
| fontSize | number | 12 | Cell size in CSS pixels. Smaller means more detail. |
| dither | "none" | "bayer" | "floyd" | "none" | Quantisation. Floyd–Steinberg for photos, Bayer for a retro look. |
| color | "ink" | "source" | string[] | "ink" | Ink uses the CSS colour (theme with text-*). Source keeps pixel colours. An array maps tone onto gradient stops, e.g. palettes.thermal. |
| edges | number | 0 | Outline contours with | / - \ where the tone gradient beats this strength. Try 0.3. Ramps only. |
| opacity | boolean | false | Fade each glyph by its tone. |
| brightness | number | 0 | Added after contrast, -1 to 1. |
| contrast | number | 1 | Multiplier around mid grey. |
| gamma | number | 1 | Tone curve exponent. |
| invert | boolean | false | Swap ink and blank. |
| animate | boolean | auto | On for fields, frames and video. Off under reduced motion, except video. |
| fps | number | 30 | Frame cap. |
| ref | Ref<AsciiHandle> | — | Exposes text(), svg(), png() and the canvas. |
role="img" and takes alt as its label. The reveal image is decorative (alt=""), so the label isn't read twice.prefers-reduced-motion it freezes on the current frame.color, so text-primary or text-foreground/40 themes them. Theme changes repaint at once.ref.current.text(), svg() or png().