Noise
BackgroundsFilm grain over a gradient or image, blended and flickering at 12 fps.
Splits text into two coloured copies and jumps a few slices sideways for a fraction of a second, every few seconds; the rest of the time the text is clean. Two pseudo-element copies clipped to slices on step-end keyframes, so nothing tweens. Pure CSS, and completely still under reduced motion.
SIGNAL
404 · not found, still measured
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 5.8 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 55 · Firefox 54 · Safari 13.1; needs clip-path.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import GlitchText from '@moonarc/core/GlitchText';
---
<h1><GlitchText text="Signal lost." /></h1>
<GlitchText text="404" period={2.5} shift={6} colorA="var(--ma-ink)" colorB="oklch(0.8 0.15 152)" />Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/glitch-text.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* GlitchText — an RGB split and a few jumping slices, every few seconds.
* The text is painted once; two pseudo-element copies (content: attr) in
* two colours are clipped to horizontal slices and offset by the hover
* travel token, on step-end keyframes so nothing tweens — a glitch that
* eases is a wobble. The burst is the last tenth of the period; the rest
* of the time the text is clean. Zero JS. Reduced motion: still.
*/
import type { HTMLAttributes, HTMLTag } from 'astro/types';
interface Props extends HTMLAttributes<'span'> {
/** The text. */
text: string;
/** Element to render. */
as?: HTMLTag;
/** Seconds between bursts. */
period?: number;
/** Horizontal offset of the split in px. Unset: the preset's --ma-travel-hover. */
shift?: number;
/** Skew of the base text during the burst, degrees. */
skew?: number;
/** Colours of the two copies. */
colorA?: string;
colorB?: string;
}
const { text, as: Tag = 'span', period = 4, shift, skew = 3, colorA = 'oklch(0.65 0.22 25)', colorB = 'oklch(0.72 0.14 210)', class: className, style, ...rest } = Astro.props;
const vars = [`--ma-glitch-period:${period}s`, shift !== undefined && `--ma-glitch-shift:${shift}px`, `--ma-glitch-skew:${skew}deg`, `--ma-glitch-a:${colorA}`, `--ma-glitch-b:${colorB}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<Tag class:list={['ma-glitch', className]} data-text={text} style={vars} {...rest}>{text}</Tag>
<style is:global>
@layer components {
:where(.ma-glitch) {
position: relative;
display: inline-block;
animation: ma-glitch-root var(--ma-glitch-period, var(--ma-dur-ambient)) step-end infinite;
}
:where(.ma-glitch)::before,
:where(.ma-glitch)::after {
/* paint only: an empty alternative after the slash keeps the two copies out of the element's accessible name, which
read the text three times. The first line is for engines without that syntax. */
content: attr(data-text);
content: attr(data-text) / '';
position: absolute;
inset: 0;
pointer-events: none;
clip-path: inset(100% 0 0 0);
animation: ma-glitch-a var(--ma-glitch-period, var(--ma-dur-ambient)) step-end infinite;
}
:where(.ma-glitch)::before {
color: var(--ma-glitch-a);
}
:where(.ma-glitch)::after {
color: var(--ma-glitch-b);
animation-name: ma-glitch-b;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-glitch) {
animation: none;
}
:where(.ma-glitch)::before,
:where(.ma-glitch)::after {
content: none;
animation: none;
}
}
}
/* the burst is the last tenth of the period; step-end so every frame is a jump */
@keyframes ma-glitch-a {
0%,
89% {
clip-path: inset(100% 0 0 0);
translate: 0 0;
}
90% {
clip-path: inset(8% 0 62% 0);
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
}
92% {
clip-path: inset(52% 0 18% 0);
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
}
94% {
clip-path: inset(24% 0 44% 0);
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
}
96% {
clip-path: inset(70% 0 6% 0);
translate: calc(-0.5 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
}
98%,
100% {
clip-path: inset(100% 0 0 0);
translate: 0 0;
}
}
@keyframes ma-glitch-b {
0%,
89% {
clip-path: inset(100% 0 0 0);
translate: 0 0;
}
90% {
clip-path: inset(60% 0 10% 0);
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
}
92% {
clip-path: inset(14% 0 66% 0);
translate: var(--ma-glitch-shift, var(--ma-travel-hover)) 0;
}
94% {
clip-path: inset(40% 0 30% 0);
translate: calc(-1 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
}
96% {
clip-path: inset(2% 0 82% 0);
translate: calc(0.5 * var(--ma-glitch-shift, var(--ma-travel-hover))) 0;
}
98%,
100% {
clip-path: inset(100% 0 0 0);
translate: 0 0;
}
}
@keyframes ma-glitch-root {
0%,
89% {
transform: none;
}
91% {
transform: skewX(calc(-1 * var(--ma-glitch-skew, 3deg)));
}
93% {
transform: skewX(var(--ma-glitch-skew, 3deg));
}
95%,
100% {
transform: none;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
text | string | none | The text; repeated by two pseudo-elements, so it must be a plain string. |
as | HTMLTag | 'span' | Element to render. |
period | number | 4 | Seconds between bursts. The burst itself is the last tenth of the period. |
shift | number | preset (ui: 4) | Horizontal offset of the slices in px. Unset, the preset's --ma-travel-hover. |
skew | number | 3 | Skew of the base text during the burst, in degrees. 0 keeps the base text still. |
colorA | string | 'oklch(0.65 0.22 25)' | Colour of the first copy. |
colorB | string | 'oklch(0.72 0.14 210)' | Colour of the second copy. |
No burst at all: the copies are removed and the text stands still. A slower glitch would still be a glitch.
ClientRouterCSS-only; nothing to rebind.
Replaces: GlitchText (React Bits) · glitch text (Kokonut, Animata). See the migration table.
Film grain over a gradient or image, blended and flickering at 12 fps.
Random characters resolve into the real text from left to right when it scrolls into view, on load, or on hover.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown