VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Text effects
  3. Decode Text

Decode Text

Monospace text that decodes from noise the first time it scrolls into view.

Text effectsClient component
··· ··························· ··· ····

Install

bash
npx @agents-npm-packages/vibeui add decode-text --dir ./src/components/ui

Import

tsx
import { DecodeText } from "@/components/ui/decode-text"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<DecodeText>

PropTypeDefaultDescription
text*string—The text to render.
classNamestring—Extra classes, merged with the component's own.
durationnumber1100Animation length in milliseconds (or seconds where noted).

Source

src/components/ui/decode-text.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

const CHARS = "abcdefghijklmnopqrstuvwxyz0123456789<>/{}[]=+*";

/** Monospace text that decodes from noise the first time it scrolls into view. */
export function DecodeText({
  text,
  className,
  duration = 1100,
}: {
  text: string;
  className?: string;
  duration?: number;
}) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const [display, setDisplay] = React.useState(() =>
    text.replace(/\S/g, "·"),
  );

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e?.isIntersecting) return;
        io.disconnect();
        const start = performance.now();
        const tick = (now: number) => {
          const t = Math.min(1, (now - start) / duration);
          const reveal = Math.floor(t * text.length);
          let out = "";
          for (let i = 0; i < text.length; i++) {
            out +=
              text[i] === " " || i < reveal
                ? text[i]
                : CHARS[Math.floor(Math.random() * CHARS.length)];
          }
          setDisplay(t < 1 ? out : text);
          if (t < 1) raf = requestAnimationFrame(tick);
        };
        raf = requestAnimationFrame(tick);
      },
      { threshold: 0.6 },
    );
    io.observe(el);
    return () => {
      io.disconnect();
      cancelAnimationFrame(raf);
    };
  }, [text, duration]);

  return (
    <span
      ref={ref}
      aria-label={text}
      className={cn("inline-block whitespace-pre font-mono text-sm text-ink", className)}
    >
      <span aria-hidden>{display}</span>
    </span>
  );
}

Exports DecodeText. Peer dependencies: clsx, tailwind-merge.

PreviousCurve TextNextFlip Text