CortexcnCortexcn

ASCII Reveal

A loader that decodes text, images, and cards into place through a character grid, with glyph skeletons while data loads.

Installation

Cortexcn
pnpm dlx shadcn@latest add @cortexcn/ascii-reveal

Usage

import { AsciiReveal, AsciiSkeleton } from "@/components/ascii-reveal";
<AsciiReveal loading={!data}>
  {data ? <Profile data={data} /> : <AsciiSkeleton lines={3} />}
</AsciiReveal>

While loading is true, the skeleton holds the space. When it turns false, the real content decodes in through a character grid in about 400 ms. Your content stays in the DOM in its real position, and the scramble is drawn on a canvas above it, so nothing shifts.

The component wraps the ascii-reveal package and themes it: glyphs take --primary, and skeletons take --muted-foreground with the rounded-md corner.

Examples

Profile card

The example above: a glyph skeleton in the card's shape, then the profile decodes in.

<AsciiReveal loading={!user}>
  {user ? (
    <ProfileCard user={user} />
  ) : (
    <div className="flex items-center gap-3">
      <AsciiSkeleton height={44} rows={5} width={44} />
      <AsciiSkeleton className="flex-1" lines={2} />
    </div>
  )}
</AsciiReveal>

Live stats

selector limits the effect to the values, so the labels stay put. On each refresh the old numbers scramble out, a shimmer runs, and the new numbers decode in.

Latency
42 ms
Uptime
99.98%
Requests
12,480
<AsciiReveal
  as="dl"
  loading={isFetching}
  revealOnMount={false}
  selector="[data-value]"
>
  <dt>Latency</dt>
  <dd data-value>{data?.latency ?? "-"}</dd>
</AsciiReveal>

Article

The scan skeleton matches the default decode preset, so the headline and body read as one wave.

<AsciiReveal loading={!post}>
  {post ? (
    <Article post={post} />
  ) : (
    <>
      <AsciiSkeleton lines={1} variant="scan" widths={["64%"]} />
      <AsciiSkeleton lines={3} variant="scan" />
    </>
  )}
</AsciiReveal>

Command output

The terminal preset leads each character with a cursor and scrambles through 0s and 1s.

<AsciiReveal loading={running} preset="terminal">
  {running ? <AsciiSkeleton lines={4} variant="terminal" /> : <Log lines={lines} />}
</AsciiReveal>

Mascot

A mascot drawn in glyphs plays in place of the shimmer, then dissolves as the content decodes in. Pick spider, ghost, pumpkin, or bat, or pass an SVG.

  • Build passed on main
  • Preview deployed
  • Two new comments
<AsciiReveal loading={isLoading} mascot="ghost">
  <Notifications items={items} />
</AsciiReveal>

Page sections

Wrap a page's sections in one AsciiReveal. The sections on screen decode as the page loads, and each one below the fold decodes when it scrolls into view. Server-rendered content stays visible on hydration unless you set revealOnHydrate.

<AsciiReveal revealOnHydrate>
  <About />
  <Experience />
  <Projects />
</AsciiReveal>

Skeleton styles

dither
scan
braille
terminal
<AsciiSkeleton variant="dither" lines={3} />
<AsciiSkeleton variant="scan" lines={3} />
<AsciiSkeleton variant="braille" lines={3} />
<AsciiSkeleton variant="terminal" lines={3} />

Marking elements

AttributeEffect
data-ascii="surface"Cards and panels: a dot field and corner frame, then a fade in
data-ascii="fade"Buttons, chips, and badges fade in on the wave
data-ascii="media"Draws any element as a glyph texture
data-ascii="skip"Leaves the subtree alone, such as a toolbar that must stay usable

Behavior

  • Screen readers read the real text the whole time, and the wrapper sets aria-busy while loading.
  • With reduced motion, the reveal becomes a short fade and the skeleton shimmer stops.
  • Skeletons are plain markup and CSS, so they render on the server and paint before any JavaScript runs.
  • On slow devices the animation finishes early and uses a short fade for the next 10 seconds.

Props

AsciiReveal

PropTypeDefaultDescription
loadingboolean-Scramble out and shimmer while true, decode in when false
preset"decode" | "dither" | "terminal" | "blueprint""decode"Glyph set, timing, and image style
duration"fast" | "cinematic""fast"fast is readable in about 400 ms
speednumber1Playback speed multiplier
origin"center" | "top-left" | "pointer" | { x, y }"center"Where the wave starts
selectorstring-Only animate content inside matching elements
revealOnMountbooleantrueReveal when the component mounts
revealOnHydratebooleanfalseAlso reveal server-rendered content
lazybooleantrueReveal sections below the fold as they scroll in
shimmerbooleantrueShow the glyph shimmer while loading
mascot"spider" | "ghost" | "pumpkin" | "bat" | string-A mascot in place of the shimmer while loading
onRevealComplete() => void-Called when a reveal finishes
asElementType"div"The wrapper element

AsciiSkeleton

PropTypeDefaultDescription
variant"dither" | "scan" | "braille" | "terminal""dither"Glyph style
linesnumber3Number of text lines
widths(string | number)[]-Line widths
rowsnumber-Draw a block of this many rows, for media
widthstring | number-Block width
heightstring | number-Block height
circlebooleanfalseRound block, for an avatar
seednumber1Varies the glyph pattern between skeletons
animatebooleantrueShimmer; turn off for long lists