Glitch text
RGB-split glitch on text — two colored ghosts jitter on a loop while the label stays sharp on top.
Installation
CLI
pnpm dlx shadcn@latest add https://animata.design/r/text/glitch-text.json
The CLI installs glitch-text.tsx and the co-located glitch-text.css in one step.
Manual
Run the following command
mkdir -p animata/text && touch animata/text/glitch-text.tsx animata/text/glitch-text.cssPaste the code
@layer components {
.glitch-text {
--glitch-intensity: 5;
--glitch-step: 0.01em;
--glitch-duration: 2.5s;
--glitch-color-base: #ffffff;
--glitch-color-a: #ff00ff;
--glitch-color-b: #00ffff;
--glitch-blend-mode: screen;
/* Derived offsets — em-relative so glitch scales with font-size. */
--glitch-x: calc(var(--glitch-intensity) * var(--glitch-step));
--glitch-y: max(var(--glitch-step), calc(var(--glitch-x) * 0.35));
--glitch-x-1: calc((var(--glitch-intensity) - 1) * var(--glitch-step));
--glitch-x-2: calc((var(--glitch-intensity) - 2) * var(--glitch-step));
position: relative;
display: inline-grid;
grid-template-areas: stack;
}
.glitch-text__ghost-a,
.glitch-text__ghost-b,
.glitch-text__base {
grid-area: stack;
}
.glitch-text__ghost-a {
color: var(--glitch-color-a);
mix-blend-mode: var(--glitch-blend-mode);
animation: glitch-a var(--glitch-duration) infinite;
}
.glitch-text__ghost-b {
color: var(--glitch-color-b);
mix-blend-mode: var(--glitch-blend-mode);
animation: glitch-b var(--glitch-duration) infinite;
}
.glitch-text__base {
position: relative;
z-index: 2;
color: var(--glitch-color-base);
}
@keyframes glitch-a {
0%,
88%,
100% {
transform: translate(0, 0);
opacity: 0.85;
}
90% {
transform: translate(calc(var(--glitch-x) * -1), calc(var(--glitch-y) * -1));
opacity: 1;
}
92% {
transform: translate(var(--glitch-x-1), var(--glitch-y));
opacity: 0.9;
}
94% {
transform: translate(calc(var(--glitch-x-2) * -1), 0);
opacity: 1;
}
96% {
transform: translate(var(--glitch-x), calc(var(--glitch-y) * -1));
opacity: 0.85;
}
}
@keyframes glitch-b {
0%,
86%,
100% {
transform: translate(0, 0);
opacity: 0.85;
}
88% {
transform: translate(var(--glitch-x), var(--glitch-y));
opacity: 1;
}
91% {
transform: translate(calc(var(--glitch-x) * -1), calc(var(--glitch-y) * -1));
opacity: 0.9;
}
93% {
transform: translate(var(--glitch-x-1), 0);
opacity: 1;
}
95% {
transform: translate(calc(var(--glitch-x-2) * -1), var(--glitch-y));
opacity: 0.85;
}
}
@media (prefers-reduced-motion: reduce) {
.glitch-text__ghost-a,
.glitch-text__ghost-b {
animation: none;
opacity: 0.35;
}
}
}"use client";
import { cn } from "@/lib/utils";
import "./glitch-text.css";
export interface GlitchTextProps extends Omit<React.ComponentPropsWithoutRef<"span">, "children"> {
children: string;
/** Unitless multiplier on `--glitch-step`. @default 5 (via CSS) */
intensity?: number;
/** Base offset unit — em scales with font size. @default "0.01em" */
step?: string;
/** Glitch burst cycle. Numbers are seconds (`2.5` → `2.5s`). @default 2.5 */
duration?: number | string;
/** Main layer color (on top). @default "#ffffff" */
baseColor?: string;
/** Back ghost layer A. @default "#ff00ff" */
colorA?: string;
/** Back ghost layer B. @default "#00ffff" */
colorB?: string;
/** Ghost layer blend mode. @default "screen" */
blendMode?: "screen" | "normal";
}
function glitchDuration(duration: number | string) {
return typeof duration === "number" ? `${duration}s` : duration;
}
/**
* RGB glitch via stacked text layers — ghosts animate transform only (compositor),
* base stays full-color on top. Props map to CSS variables; defaults live in `glitch-text.css`.
*/
export default function GlitchText({
children,
className,
intensity,
step,
duration,
baseColor,
colorA,
colorB,
blendMode,
style,
...props
}: GlitchTextProps) {
return (
<span
{...props}
className={cn("glitch-text", className)}
style={
{
...(intensity !== undefined && { "--glitch-intensity": intensity }),
...(step !== undefined && { "--glitch-step": step }),
...(duration !== undefined && {
"--glitch-duration": glitchDuration(duration),
}),
...(baseColor !== undefined && { "--glitch-color-base": baseColor }),
...(colorA !== undefined && { "--glitch-color-a": colorA }),
...(colorB !== undefined && { "--glitch-color-b": colorB }),
...(blendMode !== undefined && { "--glitch-blend-mode": blendMode }),
...style,
} as React.CSSProperties
}
>
<span aria-hidden className="glitch-text__ghost-a select-none">
{children}
</span>
<span aria-hidden className="glitch-text__ghost-b select-none">
{children}
</span>
<span className="glitch-text__base">{children}</span>
</span>
);
}Usage
import GlitchText from "@/animata/text/glitch-text";
<GlitchText className="font-(family-name:--font-mono) text-8xl font-bold tracking-tight">
404
</GlitchText>GlitchText is text only. Drop it on your own background — a gradient, a starfield, a solid block. Size it for display type; the offset is em-relative, but the effect is hard to read at body scale.
Set the readable color with baseColor (or --glitch-color-base). Ghost colors use colorA / colorB. Font, weight, and casing live on className.
<HyperspaceScatter className="min-h-[480px]">
<GlitchText
baseColor="#ffffff"
className="font-(family-name:--font-mono) text-8xl font-bold tracking-tight"
>
404
</GlitchText>
</HyperspaceScatter>Tune the burst with props — each one writes a matching --glitch-* variable on the root:
<GlitchText
baseColor="#ffffff"
intensity={8}
duration={3}
colorA="#ff00ff"
colorB="#00ffff"
className="text-6xl font-black"
>
signal lost
</GlitchText>Prefer CSS-only? Same variables on .glitch-text, or Tailwind arbitrary properties like [--glitch-intensity:8]. Pass style last if you need to override a prop.
GlitchText always renders a span. Wrap it in a heading or link when you need that semantics — do not expect as on the component.
How it works
Markup
The root is an inline grid (.glitch-text) with three stacked layers in the same cell: .glitch-text__ghost-a, .glitch-text__ghost-b, and .glitch-text__base. Each layer repeats the same string; the ghosts are aria-hidden.
Motion
Ghosts animate transform and opacity only — no layout thrash, no JS loop. The base layer sits on top at full opacity. Keyframes live in glitch-text.css.
Bursts run on a cycle (--glitch-duration, default 2.5s). Most of the time the ghosts sit still; for a few frames they snap sideways. Horizontal travel is --glitch-intensity × --glitch-step (default 5 × 0.01em), with a smaller vertical offset derived from that. Bump intensity for a harsher glitch, or step if you want a different em scale.
Ghost colors default to magenta and cyan with mix-blend-mode: screen. Change blendMode to normal on dark backgrounds if screen washes out.
Reduced motion
At prefers-reduced-motion: reduce, the ghosts stop animating and fade to 0.35 opacity — the label stays readable.
Accessibility
Screen readers get the string once from .glitch-text__base. The two ghost copies are decorative and hidden from the tree.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | — | Label copied into all three layers. |
className | string | — | Typography on the root — font, size, weight, tracking, casing. |
intensity | number | 5 | Unitless multiplier on |
step | string | "0.01em" | Base offset unit. Em-relative so the glitch scales with font size. |
duration | number | string | 2.5 | Length of one glitch cycle. Numbers count as seconds ( |
baseColor | string | "#ffffff" | Top layer — the text you actually read. |
colorA | string | "#ff00ff" | First ghost layer. |
colorB | string | "#00ffff" | Second ghost layer. |
blendMode | "screen" | "normal" | "screen" | Blend mode on both ghosts. |
style | React.CSSProperties | — | Merged after glitch props — override any |
Defaults for --glitch-intensity, --glitch-step, --glitch-duration, and the color variables also live on .glitch-text in glitch-text.css if you skip the props entirely.
Credits
Built by hari.