# Glitch Text

> A short burst of digital glitch: the word splits into two coloured channels, slices jump sideways, then it snaps back clean.

Category: Text · Free · Updated 2026-10-22 · Live demo: https://motionkitui.com/components/glitch-text

## When to use it

Tech, gaming, security and cyber themes: hover states on nav links, section labels and short headings.

## How it works

Two copies of the word sit exactly over the real one, each in one of the channel colours and blended with screen. A burst runs both through the same hand-tuned keyframes in mirror image: their clip-path cuts the word into a different horizontal slice each frame while they jump a few pixels sideways, then they fade out and the clean word remains. A fresh key replays it every time.

## Installation

```bash
npx shadcn@latest add https://motionkitui.com/r/glitch-text.json
```

Or copy `components/motionkit/glitch-text.tsx` below. Requires react, motion and Tailwind CSS v4.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` |  | The word or short phrase. |
| `trigger` | `"hover" \| "view"` | `"hover"` | Glitch on hover, or once as it scrolls in. |
| `duration` | `number` | `0.55` | Seconds the burst lasts. |
| `colors` | `[string, string]` | `["#5FAEB4", "#A56D5B"]` | The two split channels. |
| `as` | `ElementType` | `"span"` | Element to render. |

## Accessibility

- **Keyboard: Pass.** On hover mode, keyboard focus plays the burst too.
- **Screen readers: Pass.** The real word is read once; the two coloured copies are hidden.
- **Reduced motion: Pass.** With reduced motion on, there's no glitch at all.
- **Colour contrast: Pass.** The word itself never changes colour; the split channels only show for about half a second over a small area, well under WCAG 2.3.1's flash limits.

## Source

```tsx
"use client";

import { useEffect, useRef, useState, type ElementType } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

type GlitchTextProps = {
  text: string;
  /** "hover" glitches when the pointer enters or focus lands; "view" once as it scrolls in. */
  trigger?: "hover" | "view";
  /** Seconds the burst lasts. Kept short on purpose. */
  duration?: number;
  /** The two split channels. */
  colors?: [string, string];
  /** Element to render, e.g. "h2". */
  as?: ElementType;
  className?: string;
};

// where each slice cuts and how far it jumps, frame by frame: hand-tuned so the burst reads as digital noise
const slices = ["inset(8% 0 70% 0)", "inset(55% 0 20% 0)", "inset(30% 0 45% 0)", "inset(75% 0 5% 0)", "inset(15% 0 60% 0)", "inset(0 0 0 0)"];
const shifts = [-4, 5, -3, 4, -2, 0];

/**
 * A short burst of digital glitch: the word splits into two coloured channels, slices jump sideways,
 * then it snaps back clean. Brief and small by design, so it stays well clear of flashing limits.
 */
export function GlitchText({ text, trigger = "hover", duration = 0.55, colors = ["#5FAEB4", "#A56D5B"], as: Tag = "span", className = "" }: GlitchTextProps) {
  const reduceMotion = useReducedMotion();
  const ref = useRef<HTMLElement>(null);
  const inView = useInView(ref, { once: true, amount: 0.8 });
  const [burst, setBurst] = useState(0);

  // the view trigger plays one burst the first time the text is properly on screen
  useEffect(() => {
    if (trigger !== "view" || !inView || reduceMotion) return;
    const timer = window.setTimeout(() => setBurst((n) => n + 1), 150);
    return () => window.clearTimeout(timer);
  }, [trigger, inView, reduceMotion]);

  const play = () => trigger === "hover" && !reduceMotion && setBurst((n) => n + 1);
  const layer = (color: string, direction: 1 | -1) => (
    <motion.span
      key={`${color}-${burst}`}
      aria-hidden
      className="pointer-events-none absolute inset-0 select-none"
      style={{ color, mixBlendMode: "screen" }}
      initial={{ opacity: 0 }}
      animate={burst ? { opacity: [1, 1, 1, 1, 1, 0], x: shifts.map((shift) => shift * direction), clipPath: direction === 1 ? slices : [...slices].reverse() } : undefined}
      transition={{ duration, ease: "linear", times: [0, 0.2, 0.4, 0.6, 0.8, 1] }}
    >
      {text}
    </motion.span>
  );

  return (
    <Tag ref={ref} onPointerEnter={play} onFocus={play} className={`relative inline-block ${className}`}>
      {text}
      {burst > 0 && (
        <>
          {layer(colors[0], 1)}
          {layer(colors[1], -1)}
        </>
      )}
    </Tag>
  );
}
```
