# Loader Glitch Text

> Loading text that scrambles and splits into color channels, at three intensities and as a heading

A shadcn/ui block from the [@cortexcn](https://www.cortexcn.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cortexcn/loader-glitch-text-01).

- Page, with a live preview: https://shadcn.exchange/r/cortexcn/loader-glitch-text-01
- Screenshot: https://shadcn.exchange/shot/cortexcn/loader-glitch-text-01/light.png
- Registry item: https://ui.cortexcn.dev/r/loader-glitch-text-01.json
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cortexcn/loader-glitch-text-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://ui.cortexcn.dev/r/utils.json`

## Files

### `registry/blocks/loader-glitch-text-01/components/loader-glitch-text-demo.tsx`

```tsx
"use client";

import type { ReactNode } from "react";

import { LoaderGlitchText } from "./loader-glitch-text";

// Cortexcn item "loader-glitch-text-01", revision d36f60404a2f, MIT license.
// https://ui.cortexcn.dev/r/v/loader-glitch-text-01/d36f60404a2f.json

const EXAMPLES: { label: string; loader: ReactNode }[] = [
  { label: "Medium (default)", loader: <LoaderGlitchText /> },
  {
    label: "Subtle",
    loader: <LoaderGlitchText intensity="subtle" text="SYNCING" />,
  },
  {
    label: "Heavy",
    loader: <LoaderGlitchText intensity="heavy" text="DECRYPTING" />,
  },
  {
    label: "As a heading",
    loader: (
      <LoaderGlitchText
        className="font-bold text-3xl tracking-tight"
        text="BOOTING UP"
      />
    ),
  },
];

/** The glitch text loader four ways: its default, softer, harder, and large. */
export function LoaderGlitchTextDemo() {
  return (
    <div className="grid w-full max-w-3xl gap-3 sm:grid-cols-2">
      {EXAMPLES.map((example) => (
        <figure
          className="flex aspect-4/3 flex-col items-center justify-center gap-6 rounded-xl border bg-background p-6"
          key={example.label}
        >
          <div className="flex flex-1 items-center">{example.loader}</div>
          <figcaption className="font-mono text-muted-foreground text-xs">
            {example.label}
          </figcaption>
        </figure>
      ))}
    </div>
  );
}
```

### `registry/blocks/loader-glitch-text-01/components/loader-glitch-text.tsx`

```tsx
"use client";

import type { CSSProperties } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

import { cn } from "@/lib/utils";

// Cortexcn item "loader-glitch-text-01", revision d36f60404a2f, MIT license.
// https://ui.cortexcn.dev/r/v/loader-glitch-text-01/d36f60404a2f.json

const GLITCH_CHARS = "!@#$%^&*<>{}[]|/~";

interface LoaderGlitchTextProps {
  text?: string;
  intensity?: "subtle" | "medium" | "heavy";
  className?: string;
}

export type { LoaderGlitchTextProps };

const INTENSITY = {
  subtle: {
    translateScale: 1,
    duration: "4s",
    interval: 300,
    scrambleChars: 1,
  },
  medium: {
    translateScale: 1,
    duration: "2s",
    interval: 150,
    scrambleChars: 2,
  },
  heavy: {
    translateScale: 1.5,
    duration: "1s",
    interval: 80,
    scrambleChars: 3,
  },
} as const;

export function LoaderGlitchText({
  text = "LOADING",
  intensity = "medium",
  className,
}: LoaderGlitchTextProps) {
  const [displayText, setDisplayText] = useState(text);
  const textRef = useRef(text);
  const rafRef = useRef<number>(0);
  const lastScrambleRef = useRef<number>(0);
  const scrambleTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);

  textRef.current = text;

  const intensityConfig = INTENSITY[intensity];

  const scrambleText = useCallback(() => {
    const chars = textRef.current.split("");
    const indices = new Set<number>();

    while (
      indices.size < Math.min(intensityConfig.scrambleChars, chars.length)
    ) {
      indices.add(Math.floor(Math.random() * chars.length));
    }

    const scrambled = chars.map((char, i) =>
      indices.has(i) && char !== " "
        ? GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)]
        : char,
    );

    setDisplayText(scrambled.join(""));

    scrambleTimeoutRef.current = setTimeout(
      () => {
        setDisplayText(textRef.current);
      },
      50 + Math.random() * 50,
    );
  }, [intensityConfig.scrambleChars]);

  useEffect(() => {
    setDisplayText(text);
  }, [text]);

  useEffect(() => {
    let running = true;

    const animate = (timestamp: number) => {
      if (!running) {
        return;
      }

      if (timestamp - lastScrambleRef.current >= intensityConfig.interval) {
        lastScrambleRef.current = timestamp;
        scrambleText();
      }

      rafRef.current = requestAnimationFrame(animate);
    };

    rafRef.current = requestAnimationFrame(animate);

    return () => {
      running = false;
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
      }
      if (scrambleTimeoutRef.current) {
        clearTimeout(scrambleTimeoutRef.current);
      }
    };
  }, [intensityConfig.interval, scrambleText]);

  return (
    <>
      <style>{`
        @keyframes glitch-clip-1 {
          0% { clip-path: inset(40% 0 61% 0); transform: translate(calc(-2px * var(--translate-scale)), calc(-1px * var(--translate-scale))); }
          20% { clip-path: inset(92% 0 1% 0); transform: translate(calc(1px * var(--translate-scale)), calc(2px * var(--translate-scale))); }
          40% { clip-path: inset(43% 0 1% 0); transform: translate(calc(-1px * var(--translate-scale)), calc(3px * var(--translate-scale))); }
          60% { clip-path: inset(25% 0 58% 0); transform: translate(calc(3px * var(--translate-scale)), calc(-1px * var(--translate-scale))); }
          80% { clip-path: inset(54% 0 7% 0); transform: translate(calc(-2px * var(--translate-scale)), calc(2px * var(--translate-scale))); }
          100% { clip-path: inset(58% 0 43% 0); transform: translate(calc(2px * var(--translate-scale)), calc(-2px * var(--translate-scale))); }
        }

        @keyframes glitch-clip-2 {
          0% { clip-path: inset(65% 0 13% 0); transform: translate(calc(2px * var(--translate-scale)), calc(1px * var(--translate-scale))); }
          20% { clip-path: inset(79% 0 14% 0); transform: translate(calc(-3px * var(--translate-scale)), 0px); }
          40% { clip-path: inset(32% 0 52% 0); transform: translate(calc(1px * var(--translate-scale)), calc(-2px * var(--translate-scale))); }
          60% { clip-path: inset(8% 0 76% 0); transform: translate(calc(-1px * var(--translate-scale)), calc(3px * var(--translate-scale))); }
          80% { clip-path: inset(71% 0 2% 0); transform: translate(calc(3px * var(--translate-scale)), calc(1px * var(--translate-scale))); }
          100% { clip-path: inset(15% 0 62% 0); transform: translate(calc(-2px * var(--translate-scale)), calc(-1px * var(--translate-scale))); }
        }
      `}</style>
      <output
        aria-label={text}
        aria-live="polite"
        className={cn("relative inline-block font-mono", className)}
        data-slot="loader-glitch-text"
        style={
          {
            "--translate-scale": intensityConfig.translateScale,
          } as CSSProperties
        }
      >
        <span className="sr-only">{text}</span>
        <span aria-hidden="true" className="relative text-foreground">
          {displayText}
        </span>
        <span
          aria-hidden="true"
          className="absolute inset-0 text-foreground/70"
          style={{
            color: "oklch(0.65 0.2 25)",
            animation: `glitch-clip-1 ${intensityConfig.duration} infinite linear alternate-reverse`,
          }}
        >
          {displayText}
        </span>
        <span
          aria-hidden="true"
          className="absolute inset-0 text-foreground/70"
          style={{
            color: "oklch(0.7 0.15 200)",
            animation: `glitch-clip-2 ${intensityConfig.duration} infinite linear alternate-reverse`,
          }}
        >
          {displayText}
        </span>
        <span
          aria-hidden="true"
          className="pointer-events-none absolute inset-0"
          style={{
            background:
              "repeating-linear-gradient(0deg, transparent, transparent 2px, oklch(0 0 0 / 0.03) 2px, oklch(0 0 0 / 0.03) 4px)",
          }}
        />
      </output>
    </>
  );
}
```
