Skip to content
Added 13 Sept 2026
ASCII Rain
Text

ASCII Rain

Featured

A noise field resolved as monospace glyphs, ordered by ink coverage, with per-column drops falling through it. Canvas 2D, so the ramp is a string you can edit.

Textasciiterminalmatrixtextcanvasrain
lines of CSS
24lines of CSS
dependencies
0dependencies
browser, for years
Everybrowser, for years
no motion at all
Noneno motion at all
Backdrop
Canvas 2D, 3 files, no dependencies. The first one is this design. The other two are the runtime, shared by every shader effect in the catalog — copy them once and the next design you add is a single file. The HTML and CSS on this page are the preview surface and the gradient shown if WebGL is unavailable; they are not a substitute for these.
components/ascii-rain.tsx
'use client'

/**
 * ASCII Rain — a noise field resolved as monospace glyphs.
 *
 * 2D canvas rather than a shader, deliberately. A fragment shader can draw
 * ASCII by packing 5×7 bitmaps into integers and unpacking them per pixel;
 * the result works and is unreadable, and changing the character ramp means
 * re-deriving a table of magic numbers. Here the ramp is a string, the font
 * is the font, and anybody can swap either.
 *
 * Two fields are summed. A slow fbm gives the drifting mass that makes the
 * screen look like something rather than static, and a per-column "drop"
 * term — a bright head with a decaying tail — gives the rain. The ramp is
 * ordered by ink coverage, so brightness maps to how much of the cell is
 * filled, which is the whole trick behind ASCII art.
 */

import * as React from 'react'

import { ShaderSurface } from './shader-surface'
import { fbm2D, hash2D, type Canvas2DProgram } from './shader-runtime'

/** Ordered by ink coverage, lightest first. */
const RAMP = ' .:-=+*#%@'

/** Cell size in CSS pixels. Also the font size, near enough, for monospace. */
const CELL_W = 9
const CELL_H = 13

interface Column {
  /** Where this column's drop head is, in cells. */
  head: number
  speed: number
  length: number
}

export const ASCII_RAIN_PROGRAM: Canvas2DProgram = {
  id: 'ascii-rain',
  kind: 'canvas',
  speed: 1,
  light: { c0: '#94a3b8', c1: '#0f172a', c2: '#16a34a', bg: '#f8fafc' },
  dark: { c0: '#14532d', c1: '#4ade80', c2: '#dcfce7', bg: '#020803' },

  init: (frame) => {
    const cols = Math.ceil(frame.width / CELL_W)
    const rows = Math.ceil(frame.height / CELL_H)
    const columns: Column[] = []
    for (let i = 0; i < cols; i++) {
      columns.push({
        // Seeded from the column index, not from Math.random(), so a card
        // that re-seeds on resize does not visibly reshuffle.
        head: hash2D(i, 7) * rows * 2,
        speed: 4 + hash2D(i, 13) * 14,
        length: 5 + Math.floor(hash2D(i, 29) * 14),
      })
    }
    return { columns, rows, cols }
  },

  draw: (ctx, frame, state) => {
    const { columns, rows, cols } = state as { columns: Column[]; rows: number; cols: number }
    const { palette, time } = frame

    ctx.font = `${CELL_H - 1}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
    ctx.textBaseline = 'top'

    for (let x = 0; x < cols; x++) {
      const col = columns[x]
      // The head wraps through twice the height so there is a gap between
      // one drop leaving and the next arriving.
      const head = (col.head + time * col.speed) % (rows * 2)

      for (let y = 0; y < rows; y++) {
        // Ambient: the slow field everything sits in.
        const ambient = fbm2D(x * 0.12, y * 0.12 - time * 0.35) * 0.55

        // The drop: 1 at the head, decaying back up the column.
        const behind = head - y
        const drop =
          behind >= 0 && behind < col.length ? 1 - behind / col.length : 0

        const v = ambient + drop * 0.9
        if (v < 0.18) continue

        const idx = Math.min(RAMP.length - 1, Math.floor(v * RAMP.length))
        // The glyph is chosen by brightness but jittered per cell over
        // time, so a flat region shimmers instead of showing one repeated
        // character in a block.
        const jitter = Math.floor(hash2D(x, y + Math.floor(time * 6)) * 2)
        const ch = RAMP[Math.max(0, Math.min(RAMP.length - 1, idx + jitter - 1))]

        // Three bands rather than a continuous ramp: the head is the
        // accent, the tail the primary, the ambient field the muted one.
        ctx.fillStyle = drop > 0.75 ? palette.c2 : drop > 0.1 ? palette.c1 : palette.c0
        ctx.globalAlpha = Math.min(1, 0.25 + v)
        ctx.fillText(ch, x * CELL_W, y * CELL_H)
      }
    }

    ctx.globalAlpha = 1
  },
}

export function AsciiRain({ className }: { className?: string }) {
  return <ShaderSurface program={ASCII_RAIN_PROGRAM} className={className} />
}

What's included

  • The markup and the stylesheet, in whichever framework you pick
  • No runtime dependencies

Works with

  • React
  • Vue
  • Svelte
  • Tailwind
  • styled-components
  • Plain CSS

npx hoverlab add ascii-rain

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Motion safety

Not profiled. Canvas / WebGL effect: its deliverable is a shader, and the motion profile only reads CSS.

42 more effects in Text

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Text

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once