Workspace / component / text

Glitch Text

Chromatic-aberration slices that flicker on hover or on an interval, with clipped RGB ghosts offset per slice. Aggression is a single prop.

Workspacecomponenttext

paxfx add blocks/component-text-glitchlocal only

Tier: low. Reduced motion: Static text with a subtle chromatic shadow, no flicker.

"use client";

import { type CSSProperties } from "react";
import { cn, useReducedMotion } from "@pax-sh/blocks";

export interface GlitchTextProps {
  text?: string;
  /** Heading level or inline element to render. */
  as?: "h1" | "h2" | "h3" | "p" | "span";
  /** 0 to 1; scales ghost offsets and burst strength. */
  aggression?: number;
  className?: string;
}

/**
 * Chromatic-aberration slices built from two clipped ghost copies in acid
 * and mute, flickering on an idle-heavy CSS keyframe interval. Hovering
 * the text widens the offsets and speeds both loops for a stronger burst.
 * Reduced motion drops the ghosts for a static chromatic shadow.
 */
export function GlitchText({
  text = "Break the frame.",
  as: Tag = "h2",
  aggression = 0.5,
  className,
}: GlitchTextProps) {
  const reduced = useReducedMotion();
  const amp = 2 + aggression * 6;
  const burst = amp * 2.2;

  return (
    <Tag
      className={cn("m-0 font-semibold tracking-tight", className)}
      data-pax-asset="component-text-glitch"
    >
      {reduced ? (
        <span
          style={{
            textShadow:
              "0.035em 0 0 var(--pax-acid, #c8ff00), -0.035em 0 0 var(--pax-mute, #6b645c)",
          }}
        >
          {text}
        </span>
      ) : (
        <>
          <style>{`
            @keyframes pax-glitch-slice-a {
              0%, 84%, 100% { clip-path: inset(0 0 100% 0); transform: translate(0, 0); opacity: 0; }
              85% { clip-path: inset(8% 0 66% 0); transform: translate(calc(var(--pax-glitch-amp) * -1), calc(var(--pax-glitch-amp) * 0.35)); opacity: 0.9; }
              89% { clip-path: inset(52% 0 22% 0); transform: translate(var(--pax-glitch-amp), 0); opacity: 0.9; }
              93% { clip-path: inset(28% 0 48% 0); transform: translate(calc(var(--pax-glitch-amp) * 0.6), calc(var(--pax-glitch-amp) * -0.35)); opacity: 0.9; }
              96% { clip-path: inset(0 0 100% 0); transform: translate(0, 0); opacity: 0; }
            }
            @keyframes pax-glitch-slice-b {
              0%, 86%, 100% { clip-path: inset(100% 0 0 0); transform: translate(0, 0); opacity: 0; }
              87% { clip-path: inset(62% 0 10% 0); transform: translate(var(--pax-glitch-amp), calc(var(--pax-glitch-amp) * -0.35)); opacity: 0.85; }
              91% { clip-path: inset(16% 0 60% 0); transform: translate(calc(var(--pax-glitch-amp) * -1), 0); opacity: 0.85; }
              95% { clip-path: inset(44% 0 30% 0); transform: translate(calc(var(--pax-glitch-amp) * -0.6), calc(var(--pax-glitch-amp) * 0.35)); opacity: 0.85; }
              98% { clip-path: inset(100% 0 0 0); transform: translate(0, 0); opacity: 0; }
            }
            .pax-glitch-root:hover { --pax-glitch-amp: var(--pax-glitch-burst); }
            .pax-glitch-root:hover .pax-glitch-ghost-a { animation-duration: 0.9s; }
            .pax-glitch-root:hover .pax-glitch-ghost-b { animation-duration: 1.1s; }
          `}</style>
          <span
            className="pax-glitch-root relative inline-block"
            style={
              {
                "--pax-glitch-amp": `${amp}px`,
                "--pax-glitch-burst": `${burst}px`,
              } as CSSProperties
            }
          >
            <span className="relative z-[1]">{text}</span>
            <span
              aria-hidden="true"
              className="pax-glitch-ghost-a pointer-events-none absolute inset-0 select-none text-[var(--pax-acid,#c8ff00)]"
              style={{ animation: "pax-glitch-slice-a 3.4s steps(1) infinite" }}
            >
              {text}
            </span>
            <span
              aria-hidden="true"
              className="pax-glitch-ghost-b pointer-events-none absolute inset-0 select-none text-[var(--pax-mute,#6b645c)]"
              style={{ animation: "pax-glitch-slice-b 4.3s steps(1) infinite" }}
            >
              {text}
            </span>
          </span>
        </>
      )}
    </Tag>
  );
}