DistortionFree

RGB Split

White text with a red copy on the left and a cyan copy on the right that never stop shaking: the colour split of a jumping video signal, in pure CSS.

CSSGlitchText

Updated

GLITCH
Hover or click the scene to interact

This effect is free — the Distortion category contains 31 effects total, including 5 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Service status

Payments are slowSince 14:32 UTC

  • Public APIOPERATIONAL
  • DashboardOPERATIONAL
  • Payments
    DEGRADED
  • Object storageOPERATIONAL
  • Outgoing emailOPERATIONAL
  • DNSOPERATIONAL

① A hosting company's status page — the "Degraded" label of a struggling service

WhenOrvalin shows the state of its six services. Only the Payments row, slow since 14:32, carries the shaking "DEGRADED" label; the other five stay "OPERATIONAL", green and still.
WhyThe red and cyan shake suggests a link dropping out: the faulty row stands out without reading the list. Kept to one 11 px word, the effect signals without alarming, and the screen reader simply says "DEGRADED".
SettingsOn the root: --rgbs-size: 11px, --rgbs-shift: 1.5px (offset suited to small text), --rgbs-color: #b45309 (amber that reads on a light background), --rgbs-bg: transparent, --rgbs-height: auto.
Weekly sync · product team 4 people waiting
You
OFFLINE

Unstable network · retrying in 5 s

② Video call — "Offline" in the tile when the connection fails

WhenIn Talvoria, the user clicks "Join" to enter the weekly sync; the button reads "Connecting…", then their tile is covered by a dark veil with a shaking "OFFLINE" and the note "Unstable network · retrying in 5 s". The scene replays the click in a loop; on the page, you can click yourself.
WhyThe red and cyan shake is the very image of a video signal dropping out: the failure is understood before it is read, right where the picture should have appeared. The effect only serves during the incident, it does not decorate the call.
Settings--rgbs-size: 46px (34px on phones), --rgbs-bg: transparent, --rgbs-height: auto. The "Connecting…" step and the veil are wired in the scene, not in the effect.

Electronic music festival · 3rd edition

STATIKORA

June 12 → 14, 20273 stages · 41 artists

③ Electronic music festival — the name, very large, at the top of the page

WhenOn arrival, visitors see STATIKORA across the full width, with the dates, the number of stages and the "Get a pass" button below.
WhyFor an electronic music brand, a jumping signal is part of the identity: the title sets the mood on its own. Between 44 and 124 px, the flashing area stays below the threshold measured for a word that size.
Settings--rgbs-size: clamp(44px, 14.5cqw, 124px) (size following the width of the block, which has container-type: inline-size), --rgbs-bg: transparent, --rgbs-height: auto, justify-content: flex-start on the root. The rgbs-text class sits on an <h1>.

How it works

The word is written twice in the HTML: inside the .rgbs-text element and in its data-text attribute. The ::before and ::after pseudo-elements copy that attribute with content: attr(data-text), sit exactly on the text (position: absolute, same width) and go behind it with z-index: -1: only their edges show.

The red copy starts 3 px to the left, the cyan one 3 px to the right. Every 0.15 s, the rgbsShiftR animation takes the red copy 5 px left and 2 px down, rgbsShiftC takes the cyan copy 5 px right and 2 px up, then both come back. These distances are computed from --rgbs-shift (3 px): once that value, then 5/3 and 2/3 of it. No JavaScript, no interaction: the shaking starts as soon as the text is shown.

The .fx-0251-root root centres the word in a 280 px tall block on an rgb(10, 10, 15) background. Both values are variables: --rgbs-height: auto and --rgbs-bg: transparent drop the text into your own page, as in the three examples.

Accessibility

  • Screen readers: the word is read once. The copies use content: attr(data-text) / "", an empty alternative text that keeps them out of speech (Chrome 77+, Firefox 128+, Safari 17.4+). Older browsers keep the first declaration and some screen readers may announce the word three times. Measured in all three engines: "GLITCH GLITCH GLITCH" before the repair, "GLITCH" after.
  • Flashing: the coloured edges change about 7 times per second. Measured frame by frame (16 px cells, WCAG 2.3.1 criterion), the area flashing more than 3 times per second stays below the 21,824 px² threshold at the default size and up to 12rem. It gets closer if you raise --rgbs-shift on a large title (beyond about 6 px). For bigger text or a wider offset, set --rgbs-speed to 0.34s: no cell then changes more than 3 times per second (measured at 12rem).
  • Reduced motion: under prefers-reduced-motion: reduce, both animations are removed; the copies stay 3 px off each side, still. The red and cyan relief remains, with no flashing at all.
  • Contrast: the main text stays solid, the copies only peek out at the edges. On a light background, give --rgbs-color a dark colour (the status page in the examples uses #b45309).
  • No interaction: the element is neither a link nor a button and does not react to the pointer (the misleading cursor: pointer was removed). For a clickable title, put .rgbs-text on a real <a>.

Browser compatibility

Pure CSS (2.5 KB), no dependency. CSS variables, calc() inside keyframes and pseudo-elements. Measured in Chromium 151, Firefox 153 and WebKit 26.5: pixel-identical to the previous version at 0, 40, 75 and 500 ms, two independent copies, no overflow at 390 px, 0 errors.

Chrome 49+✓ Full (measured)
Firefox 31+✓ Full (measured)
Safari 9.1+✓ Full (WebKit engine measured)
Edge 79+✓ Full (Chromium engine)
Mobile iOS 9.3+✓ Full (WebKit engine measured)
Android Chrome 49+✓ Full (Chromium engine)

Without CSS variables, the rules that use them are dropped: the word shows on its own, in the page's text colour, with no copies. Without alternative text support for content, the second declaration is discarded and the copies still show.

The code

Copy the two blocks (HTML and CSS) into your page. No JavaScript, no dependencies.

HTML
<div class="fx-0251-root">
  <div class="rgbs-text" data-text="GLITCH">GLITCH</div>
</div>
CSS
.fx-0251-root,
.fx-0251-root *,
.fx-0251-root *::before,
.fx-0251-root *::after {
  box-sizing: border-box
}

/* Settings: change them on .fx-0251-root (or on a parent) */
.fx-0251-root {
  --rgbs-size: 3rem;        /* text size */
  --rgbs-color: #fff;       /* main text */
  --rgbs-red: #ff0000;      /* left copy */
  --rgbs-cyan: #00ffff;     /* right copy */
  --rgbs-shift: 3px;        /* copy offset (moves between 1 and 1.67 times this value) */
  --rgbs-speed: 0.15s;      /* duration of one shake */
  --rgbs-bg: rgb(10, 10, 15);
  --rgbs-height: 280px;     /* auto: the height of the text */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--rgbs-height);
  overflow: hidden;
  position: relative;
  background: var(--rgbs-bg);
  color: var(--rgbs-color);
  font-family: sans-serif;
}

.rgbs-text {
  font-size: var(--rgbs-size);
  font-weight: 900;
  position: relative;
  z-index: 0;
  color: var(--rgbs-color);
}

/* Coloured copies: decorative, skipped by screen readers (the / "" part) */
.rgbs-text::before,
.rgbs-text::after {
  content: attr(data-text);
  content: attr(data-text) / "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}

.rgbs-text::before {
  color: var(--rgbs-red);
  transform: translate(calc(var(--rgbs-shift) * -1), 0);
  animation: rgbsShiftR var(--rgbs-speed) infinite;
}

.rgbs-text::after {
  color: var(--rgbs-cyan);
  transform: translate(var(--rgbs-shift), 0);
  animation: rgbsShiftC var(--rgbs-speed) infinite;
}

@keyframes rgbsShiftR {

  0%,
  100% {
    transform: translate(calc(var(--rgbs-shift) * -1), 0);
  }

  50% {
    transform: translate(calc(var(--rgbs-shift) * -5 / 3), calc(var(--rgbs-shift) * 2 / 3));
  }
}

@keyframes rgbsShiftC {

  0%,
  100% {
    transform: translate(var(--rgbs-shift), 0);
  }

  50% {
    transform: translate(calc(var(--rgbs-shift) * 5 / 3), calc(var(--rgbs-shift) * -2 / 3));
  }
}

/* Reduced motion: the copies stay offset and still (no flashing) */
@media (prefers-reduced-motion: reduce) {
  .rgbs-text::before,
  .rgbs-text::after {
    animation: none;
  }
}
JavaScript

Pure CSS — no JavaScript needed.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Text (HTML — content of .rgbs-text and data-text attribute) GLITCH Write the same text in both places: the content is the word shown in white and read aloud, the attribute feeds the two coloured copies.
--rgbs-size 3rem Text size. Any CSS unit works, including clamp() or cqw to follow the block's width.
--rgbs-color #fff Main text colour. Use a dark colour on a light background.
--rgbs-red · --rgbs-cyan #ff0000 · #00ffff Colours of the left and right copies. Magenta and green (#ff00ff · #00ff66) give a more "CRT" split.
--rgbs-shift 3px Starting offset of the copies; they travel up to 5/3 of it. 1 to 1.5 px for small text, 4 to 6 px for a very large title.
--rgbs-speed 0.15s Length of one back-and-forth. 0.34s or more keeps changes under 3 per second, whatever the size.
--rgbs-bg · --rgbs-height rgb(10, 10, 15) · 280px Root background and height. transparent and auto drop the text into your layout.

FAQ

Why must the word be written twice, in the tag and in data-text?

A pseudo-element cannot copy its element's content: content: attr(data-text) only reads an attribute. If the two differ, the copies show the old word behind the new one. When a script changes the text, update the attribute at the same time: el.dataset.text = el.textContent.

Can the text wrap onto several lines?

Yes. The copies are as wide as the element and inherit its alignment, so they break at the same places as the main word. For long titles over several very large lines, slow --rgbs-speed down (0.34s) to stay below the flashing threshold.

Can the text shake only on hover?

The code as supplied shakes all the time. To keep the motion for hover, add animation-play-state: paused to both pseudo-elements, then running in a .rgbs-text:hover::before, .rgbs-text:hover::after rule; add :focus-visible if the text is a link, for keyboard users.