Aaditya

Dot Matrix Loader

Component

About

An LED sign loader for agent stages. The label scrolls across a dot matrix next to an animated glyph, and the glyph alone works inline at text size.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Dot Matrix Loader to your project and fits it to your theme.

Code

How Dot Matrix Loader is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client"

import * as React from "react"
import { DotMatrixLoader, LoaderPattern } from "@/components/ui/dot-matrix-loader"

const FIT_CSS = "html,body{overflow:hidden}[data-fit-small] [data-fit-hide]{display:none!important}"

/**
 * 21st shows a demo in a small frame: about 674 by 421 on a component page, 358 by 224 on a
 * phone and 245 by 139 on a profile card. A demo taller than its frame gets scrollbars, which
 * render white on dark pages. FitFrame fills the frame exactly instead.
 *
 * - "scale": the content shrinks until it fits, centered. For visuals such as orbs.
 * - "crop": the content stays at full size from the top and fades out at the bottom. For text
 *   that streams in, where a changing scale would make the page breathe.
 *
 * Anything marked `data-fit-hide` (state buttons, the chatcn.dev line) drops out in frames
 * shorter than `hideBelow`, so a profile card shows the visual alone.
 */
function FitFrame({
  children,
  mode = "scale",
  hideBelow = 360,
}: {
  children: React.ReactNode
  mode?: "scale" | "crop"
  hideBelow?: number
}) {
  const content = React.useRef<HTMLDivElement>(null)
  const [cropped, setCropped] = React.useState(false)

  React.useLayoutEffect(() => {
    const el = content.current
    if (!el) return
    const measure = () => {
      const vw = window.innerWidth
      const vh = window.innerHeight
      el.toggleAttribute("data-fit-small", vh < hideBelow)
      // Layout sizes ignore the transform, so measuring never feeds back.
      const w = Math.max(el.offsetWidth, el.scrollWidth)
      const h = Math.max(el.offsetHeight, el.scrollHeight)
      const scale = mode === "scale" ? Math.min(1, vw / w, vh / h) : 1
      el.style.transform = scale < 1 ? `scale(${scale})` : ""
      setCropped(mode === "crop" && h > vh + 1)
    }
    measure()
    const observer = new ResizeObserver(measure)
    observer.observe(el)
    window.addEventListener("resize", measure)
    return () => {
      observer.disconnect()
      window.removeEventListener("resize", measure)
    }
  }, [mode, hideBelow])

  const fade = "linear-gradient(to bottom, #000 calc(100% - 40px), transparent)"
  return (
    <div
      className="flex w-full justify-center overflow-hidden"
      style={{
        height: "100dvh",
        alignItems: mode === "crop" ? "flex-start" : "center",
        maskImage: cropped ? fade : undefined,
        WebkitMaskImage: cropped ? fade : undefined,
      }}
    >
      <style>{FIT_CSS}</style>
      <div
        ref={content}
        className="w-full shrink-0"
        style={{ transformOrigin: mode === "crop" ? "top center" : "center" }}
      >
        {children}
      </div>
    </div>
  )
}

/** Steps through `count` states every `ms`, for demo timelines. Stops while the tab is hidden. */
function useTicker(count: number, ms: number) {
  const [i, setI] = React.useState(0)
  React.useEffect(() => {
    const id = window.setInterval(() => {
      if (!document.hidden) setI((v) => (v + 1) % count)
    }, ms)
    return () => window.clearInterval(id)
  }, [count, ms])
  return i
}

const stages: { label: string; glyph: LoaderPattern }[] = [
  { label: "Searching the web", glyph: "orbit" },
  { label: "Reading 12 sources", glyph: "snake" },
  { label: "Drafting the report", glyph: "rain" },
  { label: "Checking citations", glyph: "pulse" },
]

/** A research agent's stages on an LED sign, and the same loader inline at reading size. */
function DotMatrixLoaderDemo() {
  const i = useTicker(stages.length, 5200)
  const stage = stages[i] ?? stages[0]
  if (!stage) return null

  return (
    <div className="flex w-full max-w-xl flex-col items-center gap-10">
      <div className="bg-(color:--chatcn-inset) rounded-(--chatcn-radius-xl) shadow-(--chatcn-shadow-border) text-(color:--chatcn-fg) flex w-fit max-w-full items-center px-6 py-5">
        <DotMatrixLoader
          key={stage.label}
          variant="both"
          glyph={stage.glyph}
          label={stage.label}
          length={9}
          size="lg"
        />
      </div>
      <div className="text-[0.875rem]/[1.25rem] tracking-[-0.003em] text-(color:--chatcn-fg-muted) flex items-center gap-3">
        <DotMatrixLoader
          variant="glyph"
          glyph={stage.glyph}
          label=""
          size={3}
          className="text-(color:--chatcn-fg)"
        />
        <span>{stage.label}</span>
      </div>
    </div>
  )
}

/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
  return (
    <p data-fit-hide className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
      {children}{" "}
      <a
        href={`https://chatcn.dev/docs/components/${slug}`}
        target="_blank"
        rel="noopener"
        className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
      >
        chatcn.dev
      </a>
    </p>
  )
}

/** A research agent's stages on an LED sign, and the same loader inline at reading size. */
export default function DotMatrixLoaderPreview() {
  return (
    <FitFrame>
      <div className="flex w-full flex-col items-center justify-center gap-8 p-6">
        <DotMatrixLoaderDemo />
        <MoreOn slug="dot-matrix-loader">ASCII, halftone and pixel loaders live on</MoreOn>
      </div>
    </FitFrame>
  )
}

More by Aaditya

View all
  • Dither Orb

    Dither Orb

    6
  • Voice Orb

    Voice Orb

    2
  • Particle Text

    Particle Text

    1
  • Agent Lanes

    Agent Lanes

    1
  • Audio Visualizer

    Audio Visualizer

    1
  • Halftone Loader

    Halftone Loader

  • ASCII Reveal

    ASCII Reveal

  • Halftone Reveal

    Halftone Reveal

Explore more

Part of chatcn — browse the full library on 21st.dev.