A dark event poster built around a glitching headline. The white text sits on top of two coloured copies of itself, one cyan and one pink, that jump and lean in opposite directions so the colours split apart like a failing screen. A stepped clip path cuts the copies into horizontal slices every few moments. Around it are a status line, a paragraph, a three row line-up, a ticket button and a control that switches the glitch between light, medium and heavy. Faint scanlines and a travelling band of light fill the background. On phones the headline shrinks to one line and the controls stack.
// Adapted from Kokonut UI (https://github.com/kokonut-labs/kokonutui/blob/main/components/kokonutui/glitch-text.tsx)
// Copyright (c) 2025 kokonutUI. MIT License: https://we.inc/components/licenses
import { useState } from "react";
import { motion, useReducedMotion } from "framer-motion";
const HEADLINE = "SIGNAL LOST";
// The two ghost copies sit behind the white text and are pushed in opposite directions.
const COLOURS = { front: "#f5f5f5", ghostA: "#22d3ee", ghostB: "#f0abfc" };
// How far the layers jump (px), how much they lean (deg) and how long one burst cycle lasts (s).
const intensities = {
light: { label: "Light", shift: 2, skew: 0.5, seconds: 2 },
medium: { label: "Medium", shift: 5, skew: 1.2, seconds: 1.1 },
heavy: { label: "Heavy", shift: 8, skew: 2.5, seconds: 0.55 },
} as const;
type Intensity = keyof typeof intensities;
const lineup = [
{ time: "21:00", act: "Mara Voss", note: "circuit-bent toy keyboards" },
{ time: "22:30", act: "Dead Pixel Choir", note: "eight broken televisions" },
{ time: "00:00", act: "Tape Hiss Collective", note: "four hour closing set" },
];
/** Keyframes for one layer. `dir` flips the path so the two ghosts never overlap. */
function jitter(shift: number, skew: number, dir: 1 | -1) {
const s = shift * dir;
return [
"translate(0px, 0px) skew(0deg)",
`translate(${s}px, ${-s}px) skew(${skew * dir}deg)`,
`translate(${-s}px, ${s}px) skew(${-skew * dir}deg)`,
`translate(${-s}px, ${-s}px) skew(${skew * dir}deg)`,
`translate(${s}px, ${s}px) skew(${-skew * dir}deg)`,
"translate(0px, 0px) skew(0deg)",
];
}
export default function GlitchTextHeadlineWithColourSplitLayers() {
const [intensity, setIntensity] = useState<Intensity>("medium");
const reduce = useReducedMotion();
const cfg = intensities[intensity];
const loop = { duration: cfg.seconds, ease: [0.25, 0.46, 0.45, 0.94] as const, repeat: Infinity };
const headlineClass = "whitespace-nowrap text-[2.5rem] font-black leading-none tracking-[0.06em] sm:text-7xl lg:text-8xl";
return (
<section className="relative w-full overflow-hidden bg-[#07070a] px-4 py-16 sm:py-20">
<style>{`
@keyframes glitch-text-headline-slice {
0%, 100% { clip-path: inset(0 0 0 0); }
12% { clip-path: inset(8% 0 62% 0); }
24% { clip-path: inset(54% 0 18% 0); }
36% { clip-path: inset(0 0 0 0); }
58% { clip-path: inset(72% 0 4% 0); }
70% { clip-path: inset(22% 0 48% 0); }
82% { clip-path: inset(0 0 0 0); }
}
@keyframes glitch-text-headline-scan {
from { transform: translateY(-100%); }
to { transform: translateY(700%); }
}
.glitch-text-headline-ghost { animation: glitch-text-headline-slice 2.6s steps(1, end) infinite; }
.glitch-text-headline-ghost-b { animation-delay: -1.1s; }
.glitch-text-headline-scan { animation: glitch-text-headline-scan 5s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.glitch-text-headline-ghost, .glitch-text-headline-scan { animation: none; }
}
`}</style>
{/* Scanlines, a slow travelling band and a vignette set the broken-monitor mood. */}
<div className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(0deg,rgba(255,255,255,0.045)_0px,rgba(255,255,255,0.045)_1px,transparent_1px,transparent_4px)]" aria-hidden="true" />
<div className="glitch-text-headline-scan pointer-events-none absolute inset-x-0 top-0 h-1/6 bg-gradient-to-b from-transparent via-cyan-300/10 to-transparent" aria-hidden="true" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,transparent_40%,rgba(0,0,0,0.75)_100%)]" aria-hidden="true" />
<div className="relative mx-auto flex max-w-5xl flex-col items-center text-center">
<p className="flex items-center gap-3 font-mono text-xs uppercase tracking-[0.3em] text-cyan-200">
<span className="h-2 w-2 rounded-full bg-fuchsia-400 motion-safe:animate-pulse" aria-hidden="true" />
Channel 04 / no carrier
</p>
<h2 className="relative mt-8 inline-block" style={{ color: COLOURS.front }}>
<motion.span
key={`front-${intensity}`}
className={`relative z-10 block ${headlineClass}`}
style={{ textShadow: "0 0 18px rgba(245,245,245,0.25)" }}
animate={reduce ? undefined : { transform: jitter(cfg.shift * 0.35, cfg.skew * 0.5, 1), opacity: [1, 0.86, 1, 0.9, 1, 1] }}
transition={loop}
>
{HEADLINE}
</motion.span>
<span className="glitch-text-headline-ghost pointer-events-none absolute inset-0 block" aria-hidden="true">
<motion.span
key={`a-${intensity}`}
className={`block ${headlineClass}`}
style={{ color: COLOURS.ghostA, textShadow: `0 0 12px ${COLOURS.ghostA}66`, transform: reduce ? `translate(${-cfg.shift}px, 0px)` : undefined }}
animate={reduce ? undefined : { transform: jitter(cfg.shift, cfg.skew, -1) }}
transition={loop}
>
{HEADLINE}
</motion.span>
</span>
<span className="glitch-text-headline-ghost glitch-text-headline-ghost-b pointer-events-none absolute inset-0 block" aria-hidden="true">
<motion.span
key={`b-${intensity}`}
className={`block ${headlineClass}`}
style={{ color: COLOURS.ghostB, textShadow: `0 0 12px ${COLOURS.ghostB}66`, transform: reduce ? `translate(${cfg.shift}px, 0px)` : undefined }}
animate={reduce ? undefined : { transform: jitter(cfg.shift, cfg.skew, 1) }}
transition={{ ...loop, delay: cfg.seconds * 0.17 }}
>
{HEADLINE}
</motion.span>
</span>
</h2>
<p className="mt-8 max-w-xl text-base leading-relaxed text-neutral-300 sm:text-lg">
Three nights of circuit-bent synths, dead-pixel visuals and tape hiss. Dockside Hall, 14 to 16 November. Doors at eight.
</p>
<ul className="mt-9 w-full max-w-xl divide-y divide-white/10 border-y border-white/10 text-left">
{lineup.map((slot) => (
<li key={slot.time} className="flex items-baseline gap-4 py-3">
<span className="font-mono text-sm text-cyan-200">{slot.time}</span>
<span className="text-sm font-semibold text-white sm:text-base">{slot.act}</span>
<span className="ml-auto hidden text-sm text-neutral-400 sm:block">{slot.note}</span>
</li>
))}
</ul>
<div className="mt-9 flex w-full flex-col items-center justify-center gap-4 sm:w-auto sm:flex-row">
<a href="#" className="inline-flex h-12 w-full items-center justify-center bg-white px-8 font-mono text-sm font-bold uppercase tracking-widest text-black transition hover:bg-cyan-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 focus-visible:ring-offset-2 focus-visible:ring-offset-[#07070a] sm:w-auto">
Get a wristband
</a>
<div className="flex items-center border border-white/20" role="group" aria-label="Glitch strength">
{(Object.keys(intensities) as Intensity[]).map((name) => (
<button
key={name}
type="button"
aria-pressed={intensity === name}
onClick={() => setIntensity(name)}
className={`h-12 px-4 font-mono text-xs uppercase tracking-widest transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cyan-300 ${intensity === name ? "bg-fuchsia-300 text-black" : "text-neutral-200 hover:bg-white/10"}`}
>
{intensities[name].label}
</button>
))}
</div>
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/glitch-text-headline-with-colour-split-layers.json
The same text is stacked three times in the same place. The top copy is the readable one. The two below it are tinted and moved a few pixels in opposite directions on a fast loop, so their colours peek out from different sides on each frame.
Each coloured copy sits in a wrapper with a `clip-path: inset(...)` animation that uses `steps(1)` timing, so it snaps between bands instead of sliding. The second wrapper has a negative delay so the two never cut at the same height.
Translate and skew have to change together on the same frame for the jump to look sharp. Animating the full `transform` value keeps them in step.
Yes. Keep the `animate` props undefined by default and set them from a hover state, the same way the component already switches them off for reduced motion.
It is meant for one to three words. For a sentence, lower the shift to one or two pixels or apply the effect to a single word inside it.
Adapted from Glitch Text in Kokonut UI, used under its open-source licence. Licences and credits
Related: Text Scramble Decode Headline, Shimmering Chrome Gradient Headline, Typewriter Headline with Cycling Phrases.
More text effects or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.