A controlled flicker animation for cinematic hero sections, cyber interfaces, launch pages, music campaigns, game-adjacent sites, and moody editorial layouts where typography needs atmosphere without losing readability.

Flickering Text gives display typography a short burst of unstable light.
Letters brighten, dim, stutter, and settle into a readable final state. The effect works because the instability is brief. It creates tension, then gets out of the way before the visitor has to work too hard to read the message.
Use Flickering Text when a headline should feel cinematic, electrical, haunted, technical, or signal-led. It fits dark hero sections, campaign intros, entertainment pages, cyber brands, AI product launches, portfolio openers, and visual storytelling sections where the type can carry a moment of controlled distortion.
The page job is atmosphere. The visitor should feel the mood before reading the rest of the page, but the final headline still needs to be legible, stable, and worth the attention it just demanded.
npx hyperiux add flickering-textimport FlickeringText from "@/components/effects/flickering-text";
const page = () => {
return (
<>
<FlickeringText/>
</>
)
}
export default page
// Built using Hyperiux Vault: https://vault.hyperiux.com
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(useGSAP, SplitText);
// The shared noise lane every character blends toward. Character lanes start
// at 1 so lane 0 is always the common stream.
const SHARED_LANE = 0;
// Hash constants for the deterministic value noise. Odd primes, nothing more.
const HASH_LANE = 374761393;
const HASH_STEP = 668265263;
const HASH_SEED = 2246822519;
const HASH_MIX = 1274126177;
const UINT32_MAX = 4294967295;
const LINE_HEIGHT = 0.82;
// Bloom stack, tightest to widest. Radii are in em so the halo tracks the
// font size, and the alphas stack up into a soft falloff instead of a ring.
const GLOW_LAYERS = [
{ radius: 0.08, alpha: 0.55 },
{ radius: 0.22, alpha: 0.35 },
{ radius: 0.5, alpha: 0.22 },
];
const DEFAULT_LINES = ["FLICKERING", "TEXT"];
const DEFAULT_RANDOM_SEED = 0;
const DEFAULT_GLOW_THRESHOLD = 0.6;
/** Deterministic value in [-1, 1] for a lane at a discrete wiggle step. */
const randomAt = (lane, step, seed) => {
let hash =
(Math.imul(lane, HASH_LANE) ^
Math.imul(step, HASH_STEP) ^
Math.imul(seed, HASH_SEED)) >>>
0;
hash = Math.imul(hash ^ (hash >>> 13), HASH_MIX) >>> 0;
return (hash / UINT32_MAX) * 2 - 1;
};
/** Layered text-shadow for a bloom at the given strength, 0 to 1. */
const glowShadow = (strength, color, radius) => {
if (strength <= 0) return "none";
return GLOW_LAYERS.map(({ radius: layerRadius, alpha }) => {
const mix = (alpha * strength * 100).toFixed(1);
return `0 0 ${(layerRadius * radius).toFixed(3)}em color-mix(in srgb, ${color} ${mix}%, transparent)`;
}).join(", ");
};
/** Smoothstep-interpolated value noise: the wiggle between two samples. */
const noiseAt = (lane, time, seed) => {
const step = Math.floor(time);
const t = time - step;
const ease = t * t * (3 - 2 * t);
const from = randomAt(lane, step, seed);
const to = randomAt(lane, step + 1, seed);
return from + (to - from) * ease;
};
const FlickeringText = ({
className = "",
backgroundColor = "#111844",
textColor = "#ffffff",
wigglesPerSecond = 2,
correlation = 0.5,
minOpacity = 0,
minAmount = -1,
maxAmount = 1,
fontSize = "10vw",
fontWeight = 700,
glow = true,
glowColor,
glowIntensity = 1,
glowRadius = 1,
}) => {
const bloomColor = glowColor ?? textColor;
const containerRef = useRef(null);
const charRefs = useRef([]);
const lineRefs = useRef([]);
const lines = DEFAULT_LINES;
useGSAP(
() => {
const splits = lineRefs.current
.filter((line) => Boolean(line))
.map((line) =>
SplitText.create(line, {
type: "chars",
charsClass: "flickering-text-char",
aria: "none",
}),
);
const chars = splits.flatMap((split) => split.chars);
charRefs.current = chars;
if (!chars.length) return;
const prefersReduced = window.matchMedia?.(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReduced) {
chars.forEach((char) => {
char.style.opacity = "1";
char.style.textShadow = "none";
});
return () => {
splits.forEach((split) => split.revert());
charRefs.current = [];
};
}
const startTime = gsap.ticker.time;
const blendNorm = Math.hypot(1 - correlation, correlation);
const glowRamp = Math.max(1 - DEFAULT_GLOW_THRESHOLD, 0.001);
const onTick = () => {
const time = (gsap.ticker.time - startTime) * wigglesPerSecond;
const shared = noiseAt(SHARED_LANE, time, DEFAULT_RANDOM_SEED);
chars.forEach((char, lane) => {
const own = noiseAt(lane + 1, time, DEFAULT_RANDOM_SEED);
const noise =
(own * (1 - correlation) + shared * correlation) / blendNorm;
const amount =
((noise + 1) / 2) * (maxAmount - minAmount) + minAmount;
const opacity = gsap.utils.clamp(
Math.min(minOpacity, 1),
1,
1 + amount * (minOpacity - 1),
);
const strength = glow
? gsap.utils.clamp(
0,
1,
(opacity - DEFAULT_GLOW_THRESHOLD) / glowRamp,
) * glowIntensity
: 0;
char.style.opacity = String(opacity);
char.style.textShadow = glowShadow(strength, bloomColor, glowRadius);
});
};
gsap.ticker.add(onTick);
return () => {
gsap.ticker.remove(onTick);
splits.forEach((split) => split.revert());
charRefs.current = [];
};
},
{
scope: containerRef,
dependencies: [
wigglesPerSecond,
correlation,
minOpacity,
minAmount,
maxAmount,
glow,
bloomColor,
glowIntensity,
glowRadius,
],
},
);
return (
<section
className={`relative flex min-h-screen w-full items-center justify-center overflow-hidden px-4 sm:px-6 ${className}`}
style={{
backgroundColor: backgroundColor,
}}
>
<div
aria-hidden="true"
className="absolute inset-0"
style={{
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
}}
/>
<div
ref={containerRef}
className="relative w-full -translate-y-32 max-w-[1400px]"
aria-label={DEFAULT_LINES.join(" ")}
>
{lines.map((line, lineIndex) => (
<p
key={`line-${lineIndex}`}
aria-hidden="true"
ref={(el) => {
lineRefs.current[lineIndex] = el;
}}
className="m-0 w-full whitespace-nowrap text-center uppercase tracking-[-0.02em]"
style={{
color: textColor,
lineHeight: LINE_HEIGHT,
fontSize,
fontWeight,
}}
>
{line}
</p>
))}
</div>
</section>
);
};
export default FlickeringText;A cyber product page can use Flickering Text in the hero to introduce a short headline with controlled instability. The text pulses, settles, and gives the page a darker technical tone before the supporting copy and CTA appear.
The outcome is tension. The brand feels sharper, but the message still resolves into a clear claim.
A film, music, or campaign microsite can use the same effect for an opening title where atmosphere matters before explanation. The flicker earns attention without forcing the whole page into constant motion.
Not for body copy, documentation, legal text, pricing details, error messages, form labels, instructions, or accessibility-critical content.
Not for long loops, constant blinking, or repeated flicker effects across the page.
Not for calm SaaS pages where trust, clarity, and reading speed matter more than atmosphere.
Keep the effect scoped to one or two headlines. Animate opacity, text-shadow, filter, or layered glow with restraint, and avoid large blur values on multiple text layers. Clean up timers or animation instances when the component unmounts.
Do not loop the flicker indefinitely near CTAs, forms, or reading sections. Once the headline has arrived, let it stay still.
Expose the headline once as readable text. Do not make assistive technology read duplicate glow layers or flicker fragments.
Avoid fast, high-contrast flashes. On mobile, reduce glow intensity, blur, and flicker count so the text remains comfortable to read. For reduced motion, render the stable headline immediately or use a minimal fade with no flicker.
| Prop | Type | Default | Description |
|---|---|---|---|
backgroundColor | string | #111844 | Background color behind the text. |
textColor | string | #ffffff | Text color. |
glowColor | string | #ffffff | Glow color when bloom is enabled. |
wigglesPerSecond | number | 2 | Noise samples per second that drive the flicker. |
correlation | number | 0.5 | How much each character follows the shared flicker lane. |
minOpacity | number | 0 | Lowest opacity characters can dip to. |
minAmount | number | -1 | Lower bound of the flicker selector. |
maxAmount | number | 1 | Upper bound of the flicker selector. |
glowIntensity | number | 1 | Glow strength when bloom is enabled. |
glowRadius | number | 1 | Glow spread multiplier. |
fontSize | number | string | 12vw | Font size for the text. |
fontWeight | number | string | 600 | Font weight for the text. |
glow | boolean | true | Turns the extra glow effect on or off. |
Flickering Text is a React text animation that makes a headline pulse, dim, brighten, or stutter briefly before settling into a stable readable state.
Use it when a hero, campaign title, or editorial intro needs cinematic, cyber, electrical, or signal-led atmosphere.
No. Flicker belongs on short display text. Body copy should stay stable and easy to read.
Render the headline as real text, expose it once to assistive technology, and hide decorative duplicate glow layers where needed.
Usually no. It should flicker briefly, resolve, and stop. Continuous blinking quickly becomes distracting and uncomfortable.
Show the final stable headline immediately or with a minimal fade. Remove flicker, glow pulses, opacity stutters, and repeated light changes.
Yes, with restraint. Reduce glow intensity, flicker count, blur, and contrast shifts so the headline remains readable on smaller screens.
Yes. A custom version should define flicker rhythm, glow treatment, contrast limits, trigger behavior, mobile tuning, accessibility handling, reduced-motion fallback, source handoff, and implementation notes.
Need a custom effect? Tell us what to create.