GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Glitch Effect Transitions (No Codepen)

This is a css glitch effect: An offset value pulls RGB channels apart on load, then back together.

Auto-plays · click a tile to jump to its section · all nine in one zip

Nine is not an arbitrary round number of patterns here — the order moves from a single loaded image (01) through screen-wide slices (02, 03), a brightness-driven reveal (04), a short content flash (05), two color-only drifts (06, 07), a hard structural split (08), and finally a text-only transition (09), so each one demonstrates one more way a plain JavaScript number can drive a canvas frame.

Demo

The 3×3 grid above is nine independent canvas elements, each running its own requestAnimationFrame loop. This isn't a css glitch effect codepen embed pointing at someone else's pen — every tile is this article's own canvas, and the exact code behind it is printed under each pattern below. Click a tile, press Enter or Space on it, or resize the window, and that tile's RENDER(p) function walks the pixel buffer for the frame at hand; nothing here waits on a CSS transitionend event, because there is no CSS transition running underneath any of it.

Why Nine Glitch Transitions Compute Every Pixel Instead of Animating a Property

A CSS transition can only interpolate a property's own value — a color, an offset, an opacity — from one number to another. It can't read a single pixel's byte, decide which source pixel to copy in its place, or sort a row of pixels by brightness. getImageData hands the canvas the raw byte array for a frame, and putImageData writes a new one back, so the nine patterns below can each drive that per-pixel choice from one plain number, p, recomputed every animation frame. None of the nine reach for Math.random either: a seeded generator (makeLcg) builds any fixed layout — block offsets, row jitter, noise seeds — once, so the same pattern replays identically every time instead of reshuffling on each run. There's no library, no WebGL context, and no CDN script anywhere in the nine files behind this article.

Anatomy of Nine CSS Glitch Effect Transitions — Where the Calculation Runs

The screen-recording measurements below come straight from the nine demo files, not an estimate: cumulative area is how much of the canvas changed across the whole transition, average intensity is the average per-pixel byte difference where it changed, and frames moved counts how many of the 23 frame-to-frame deltas actually moved a pixel.

# Pattern Cumulative area Avg. intensity Frames moved
01 Image Transition Where a Channel-offset Value Splits Then Rejoins 48.7% 45.8 23 of 23
02 Screen Transition Where Row-displacement Values Settle Back 28.9% 105.9 23 of 23
03 Screen Transition Where Block-coordinate Values Scatter Then Match 32.6% 134.4 23 of 23
04 Image Transition Where a Brightness-sort Progress Value Reverses 28.2% 149.6 23 of 23
05 Content Transition Where a Noise-intensity Value Fades Out 48.8% 29.7 22 of 23
06 Screen Transition Where a Chromatic-aberration Amplitude Value Decays 38.4% 74.9 23 of 23
07 Screen Transition Where a Tracking-wobble Value Settles 44.6% 212.2 23 of 23
08 Screen Transition Where a Tear-offset Value Locks Back Together 37.6% 195.6 21 of 23
09 Text Transition Where a Decode-progress Value Resolves Real Letters 19.3% 50.6 22 of 23

Code Per Pattern

Each pattern below runs the same three-step shape — read the source frame, compute a destination byte, write it back — with a different rule for step two. The ranges are pulled straight from the demo source, unmodified.

01Image Transition Where a Channel-offset Value Splits Then Rejoins

A channel-offset value pushes the red and blue channels apart the instant the image appears while green stays anchored; ROW_JITTER varies that offset per row so the split doesn't read as one flat shear, and BRIGHT_PULSE and GHOST_MIX add a short brightness pulse and a ghosted overlap as the channels pull apart. Click the tile, or tab to it and press Enter, to replay the load.

Image Transition Where a Channel-offset Value Splits Then Rejoins
Open in a new tab

01 · Image Transition Where a Channel-offset Value Splits Then Rejoins — Measured area 48.733% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RGB_OFFSET_MAXgetImageDataputImageData
    var pulse = 1 + BRIGHT_PULSE * Math.sin(Math.PI * p);
    for (var y = 0; y < ih; y++) {
      var rowOffset = Math.round(offset * rowJitter[y]);
      var gy = clampI(y - voffset, 0, ih - 1);
      for (var x = 0; x < iw; x++) {
        var rx = clampI(x - rowOffset, 0, iw - 1);
        var bx = clampI(x + rowOffset, 0, iw - 1);
        var rx2 = clampI(x - Math.round(rowOffset * 1.6), 0, iw - 1);
        var bx2 = clampI(x + Math.round(rowOffset * 1.6), 0, iw - 1);
        var k = (y * iw + x) * 4;
        var kr = (y * iw + rx) * 4;
        var kr2 = (y * iw + rx2) * 4;
        var kg = (gy * iw + x) * 4;
        var kb = (y * iw + bx) * 4;
        var kb2 = (y * iw + bx2) * 4;
        o[k] = clampI(Math.round((data[kr] * (1 - GHOST_MIX) + data[kr2] * GHOST_MIX) * pulse), 0, 255);
        o[k + 1] = clampI(Math.round(data[kg + 1] * pulse), 0, 255);
        o[k + 2] = clampI(Math.round((data[kb + 2] * (1 - GHOST_MIX) + data[kb2 + 2] * GHOST_MIX) * pulse), 0, 255);
        o[k + 3] = 255;
      }
    }

02Screen Transition Where Row-displacement Values Settle Back

PRECOMPUTE() rolls a fixed rowOffsets value for every row with the seeded RNG once, and RENDER(p) multiplies each row's stored offset by the current progress before copying pixels from the shifted source row into place. Because the offsets are computed once and only scaled by p, the same jagged shape returns on every replay instead of reshuffling.

Screen Transition Where Row-displacement Values Settle Back
Open in a new tab

02 · Screen Transition Where Row-displacement Values Settle Back — Measured area 28.934% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rowOffsetsstripeWputImageData
  function PRECOMPUTE() {
    iw = cv.width; ih = cv.height;
    data = bctx.getImageData(0, 0, iw, ih).data;
    outImg = ctx.createImageData(iw, ih);
    var rng = makeLcg(SEED + 7);
    rowOffsets = new Float32Array(ih);
    for (var y = 0; y < ih; y++) rowOffsets[y] = (rng() * 2 - 1) * ROW_OFFSET_MAX * dpr;
  }

  function clampI(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); }

  function RENDER(p) {
    var o = outImg.data;
    for (var y = 0; y < ih; y++) {
      var shift = Math.round(rowOffsets[y] * p);
      var rowBase = y * iw;
      for (var x = 0; x < iw; x++) {
        var sx = clampI(x - shift, 0, iw - 1);
        var k = (rowBase + x) * 4;
        var sk = (rowBase + sx) * 4;
        o[k] = data[sk]; o[k + 1] = data[sk + 1]; o[k + 2] = data[sk + 2]; o[k + 3] = 255;
      }
    }
    ctx.putImageData(outImg, 0, 0);
  }

03Screen Transition Where Block-coordinate Values Scatter Then Match

The canvas splits into BLOCK_ROWS by BLOCK_COLS squares, and each block gets its own blockDx/blockDy scatter distance computed once; RENDER(p) works from p back toward zero, so every block's pixels sample from a shrinking offset until the block lands on its real coordinate. Resize the window and the same block grid rebuilds at the new size.

Screen Transition Where Block-coordinate Values Scatter Then Match
Open in a new tab

03 · Screen Transition Where Block-coordinate Values Scatter Then Match — Measured area 32.552% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

blockDxblockDyputImageData
  function RENDER(p) {
    var o = outImg.data;
    for (var y = 0; y < ih; y++) {
      var by = Math.min(BLOCK_ROWS - 1, Math.floor(y / blockH));
      for (var x = 0; x < iw; x++) {
        var bx = Math.min(BLOCK_COLS - 1, Math.floor(x / blockW));
        var bi = by * BLOCK_COLS + bx;
        var sx = clampI(Math.round(x - blockDx[bi] * p), 0, iw - 1);
        var sy = clampI(Math.round(y - blockDy[bi] * p), 0, ih - 1);
        var k = (y * iw + x) * 4;
        var sk = (sy * iw + sx) * 4;
        o[k] = data[sk]; o[k + 1] = data[sk + 1]; o[k + 2] = data[sk + 2]; o[k + 3] = 255;
      }
    }
    ctx.putImageData(outImg, 0, 0);
  }

04Image Transition Where a Brightness-sort Progress Value Reverses

PRECOMPUTE() reads every pixel's brightness in a row and sorts that row's x-positions by it into sortedIdx, so the darkest and brightest pixels end up grouped at opposite ends; a progress value then blends between that sorted order and the true order as it drops, which is what produces the streaked bands before the image locks into focus.

Image Transition Where a Brightness-sort Progress Value Reverses
Open in a new tab

04 · Image Transition Where a Brightness-sort Progress Value Reverses — Measured area 28.2% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

sortedIdxbrightnessclampI
  function PRECOMPUTE() {
    iw = cv.width; ih = cv.height;
    data = bctx.getImageData(0, 0, iw, ih).data;
    outImg = ctx.createImageData(iw, ih);
    sortedIdx = [];
    for (var y = 0; y < ih; y++) {
      var row = new Int32Array(iw);
      var order = [];
      for (var x = 0; x < iw; x++) order.push(x);
      var base = y * iw;
      order.sort(function (a, b) { return brightness((base + a) * 4) - brightness((base + b) * 4); });
      for (var j = 0; j < iw; j++) row[j] = order[j];
      sortedIdx.push(row);
    }
  }

05Content Transition Where a Noise-intensity Value Fades Out

Every pixel gets its own noise color from hash01 — a deterministic hash of the pixel's coordinates and the current frame count, not Math.random — and that noise color blends against the real pixel by a value p that spikes toward 1 and fades back. Turning NOISE_BURST_DURATION up only changes how many frames p spends near 1 before the content underneath becomes clear again.

Content Transition Where a Noise-intensity Value Fades Out
Open in a new tab

05 · Content Transition Where a Noise-intensity Value Fades Out — Measured area 48.761% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

NOISE_BURST_DURATIONframeCounterhash01
    for (var y = 0; y < ih; y++) {
      var ySalt = Math.imul(y, 668265263) >>> 0;
      var base = y * iw;
      for (var x = 0; x < iw; x++) {
        var seed = (Math.imul(x, 374761393) ^ ySalt ^ fSalt) >>> 0;
        var h0 = hash01(seed);
        var h1 = hash01(seed ^ 0x9e3779b9);
        var h2 = hash01(seed ^ 0x85ebca6b);
        var h3 = hash01(seed ^ 0xc2b2ae35);
        var g = 198 + h0 * 57;
        var nr = clamp255(g + (h1 - 0.5) * 60);
        var ng = clamp255(g + (h2 - 0.5) * 60);
        var nb = clamp255(g + (h3 - 0.5) * 60);
        var k = (base + x) * 4;
        o[k] = data[k] * (1 - p) + nr * p;
        o[k + 1] = data[k + 1] * (1 - p) + ng * p;
        o[k + 2] = data[k + 2] * (1 - p) + nb * p;
        o[k + 3] = 255;
      }
    }

06Screen Transition Where a Chromatic-aberration Amplitude Value Decays

dxR, dxB, and dyG hold a per-channel pixel offset built from CHROMATIC_DRIFT_AMPLITUDE, and RENDER(p) samples red, green, and blue from three different source coordinates before writing them into the same output pixel, which is what separates the channels in the first place. An easeInOut curve decays that amplitude back toward zero rather than cutting it instantly, so the merge reads as a settle instead of a snap.

Screen Transition Where a Chromatic-aberration Amplitude Value Decays
Open in a new tab

06 · Screen Transition Where a Chromatic-aberration Amplitude Value Decays — Measured area 38.372% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CHROMATIC_DRIFT_AMPLITUDEdxReaseInOut
    for (var y = 0; y < ih; y++) {
      var base = y * iw;
      var gy = clampI(y + dyG, 0, ih - 1);
      var gBase = gy * iw;
      for (var x = 0; x < iw; x++) {
        var rx = clampI(x - dxR, 0, iw - 1);
        var gx = clampI(x, 0, iw - 1);
        var bx = clampI(x - dxB, 0, iw - 1);
        var k = (base + x) * 4;
        var kr = (base + rx) * 4;
        var kg = (gBase + gx) * 4;
        var kb = (base + bx) * 4;
        o[k] = data[kr];
        o[k + 1] = data[kg + 1];
        o[k + 2] = data[kb + 2];
        o[k + 3] = 255;
      }
    }

07Screen Transition Where a Tracking-wobble Value Settles

A wobble value drives rollY up and down on a sine wave, so RENDER(p) reads every row from a vertically wrapped source position — wrap() keeps that row index inside the canvas instead of running off the edge — and adds a smaller per-row jitterX from the same hash01 pattern that item 05 uses. TAPE_WOBBLE_AMPLITUDE sets how far that sine wave can push a row before the value dies down to a steady image.

Screen Transition Where a Tracking-wobble Value Settles
Open in a new tab

07 · Screen Transition Where a Tracking-wobble Value Settles — Measured area 44.612% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TAPE_WOBBLE_AMPLITUDErollYwrap
  function RENDER(p) {
    phaseRoll += 0.33;
    frameCounter = (frameCounter + 1) >>> 0;
    var rollY = Math.round(TAPE_WOBBLE_AMPLITUDE * dpr * p * Math.sin(phaseRoll));
    var fSalt = Math.imul(frameCounter, 2246822519) >>> 0;
    var o = outImg.data;
    for (var y = 0; y < ih; y++) {
      var srcY = wrap(y - rollY, ih);
      var jh = hash01((Math.imul(srcY, 668265263) ^ fSalt) >>> 0);
      var jitterX = Math.round(TAPE_JITTER_AMPLITUDE * dpr * p * (jh * 2 - 1));
      var base = y * iw;
      var srcBase = srcY * iw;
      for (var x = 0; x < iw; x++) {
        var sx = clampI(x - jitterX, 0, iw - 1);
        var k = (base + x) * 4;
        var sk = (srcBase + sx) * 4;
        o[k] = data[sk]; o[k + 1] = data[sk + 1]; o[k + 2] = data[sk + 2]; o[k + 3] = 255;
      }
    }
    ctx.putImageData(outImg, 0, 0);
  }

08Screen Transition Where a Tear-offset Value Locks Back Together

tearY splits the canvas into a top half and a bottom half once, and a single tear-offset value shifts the two halves in opposite directions; RENDER(p) scales that offset by p, so as p drops to zero the two fragments slide back until every row lines up and the halves read as one screen again. An error state or a full-screen menu can reuse the same split for an abrupt swap.

Screen Transition Where a Tear-offset Value Locks Back Together
Open in a new tab

08 · Screen Transition Where a Tear-offset Value Locks Back Together — Measured area 37.565% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

TEAR_OFFSET_MAXtearYclampI
  function RENDER(p) {
    var off = Math.round(TEAR_OFFSET_MAX * dpr * p);
    var o = outImg.data;
    for (var y = 0; y < ih; y++) {
      var dx = y < tearY ? off : -off;
      var base = y * iw;
      for (var x = 0; x < iw; x++) {
        var sx = clampI(x - dx, 0, iw - 1);
        var k = (base + x) * 4;
        var sk = (base + sx) * 4;
        o[k] = data[sk]; o[k + 1] = data[sk + 1]; o[k + 2] = data[sk + 2]; o[k + 3] = 255;
      }
    }
    ctx.putImageData(outImg, 0, 0);
  }

09Text Transition Where a Decode-progress Value Resolves Real Letters

Instead of getImageData, RENDER(p) draws straight with ctx.fillText: a decode-progress value compares against DECODE_SPEED for each character index, and until a character's turn comes up it draws a random pool character from POOL at a small hash-driven jitter offset in GHOST_COLOR, then switches to the real character in TEXT_COLOR once resolved. Lowering DECODE_SPEED stretches how many frames a character spends scrambled before it locks in from the left.

Text Transition Where a Decode-progress Value Resolves Real Letters
Open in a new tab

09 · Text Transition Where a Decode-progress Value Resolves Real Letters — Measured area 19.26% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

DECODE_SPEEDcharXctx.fillText
    for (var i = 0; i < n; i++) {
      var resolved = decodeFrac * DECODE_SPEED * n >= (i + 1);
      var ch = chars[i];
      var drawCh = ch;
      var jy = 0;
      if (!resolved && ch !== ' ') {
        var seed = (Math.imul(i + 1, 2654435761) ^ Math.imul(frameCounter, 2246822519) ^ SEED) >>> 0;
        var hIdx = Math.floor(hash01(seed) * POOL.length);
        drawCh = POOL[hIdx];
        jy = Math.round((hash01(seed ^ 0x9e3779b9) - 0.5) * 5 * p);
        ctx.fillStyle = GHOST_COLOR;
        boundaryX = charX[i];
      } else {
        ctx.fillStyle = TEXT_COLOR;
      }
      ctx.fillText(drawCh, charX[i], baselineY + jy);
    }

What Changes When RGB_OFFSET_MAX, NOISE_BURST_DURATION, or DECODE_SPEED Moves

Three constants below were each moved once to see how far a single number can push the feel of a pattern without touching anything else in the file.

Variant Changed value What it feels like
Adjust the maximum RGB-channel offset value 01 RGB_OFFSET_MAX 24px → 60px Raising the maximum from 24px to 60px widens how far the R/B channels split apart at load before returning to 0, making the split feel rougher.
Adjust the noise-burst duration value 05 NOISE_BURST_DURATION 560ms → 900ms Lengthening the duration from 560ms to 900ms makes the noise take longer to fade, so the white-out lingers longer.
Adjust the text decode-speed value 09 DECODE_SPEED 1.7 → 1.0 Lowering the decode speed from 1.7 to 1.0 increases the number of frames each character spends scrambled before resolving, visibly slowing the left-to-right lock-in.

A Product Image, a Notification Banner, and a Login Message Actually Use These

Pattern 01's channel split is built for a moment where a real image is loading in — a product detail image or a portfolio thumbnail — because it only needs the loaded bitmap itself, not a second image to cut away to. Swap the click trigger for a css glitch effect on hover interaction instead, and the same RENDER(p) runs on pointer-enter on the thumbnail rather than a click.

Pattern 05's noise burst reads best on a short, low-stakes content change — a tab switch or a notification banner sliding in — because a full second of static would read as an error rather than a transition; that's also why NOISE_BURST_DURATION defaults to well under a second rather than over one.

Pattern 09 is the one built for words: a hero headline on load, or a login-success message replacing a spinner. Nothing about DECODE_SPEED or POOL depends on a specific string, so the same decode can run on any short line of text a screen needs to reveal one letter at a time.

Where a CSS Glitch Effect Breaks — Motion, Contrast, and Keyboard Users

Every one of the nine reads prefers-reduced-motion once when the page loads, not on every frame, and when that media query reports reduce it renders a single still frame — RENDER(0.5) or RENDER(1) depending on the pattern — instead of looping; none of the nine animate on a timer that ignores the setting. Where it actually breaks: pattern 07's wobble and pattern 03's block scatter both move the largest share of the canvas, so on an underpowered device the frame time can slip enough that a settle starts to look like a stutter — keep BLOCK_ROWS and BLOCK_COLS modest if the target screen is wide. Every clickable tile also carries role="button", tabindex="0", and an aria-label, and the same handler answers a click, an Enter key, and a Space key, so a keyboard-only visitor can replay any of the nine without a mouse. The nine source files, one per pattern, sit behind 8mgtprm2 in the same folder as this article's demo — open one before widening a value, since a tear or a scatter that reads fine at its default offset can clip off-canvas at a much larger one.

Three Variants and Accessibility

All three variants in the table above only change one numeric constant, so none of them add new motion a screen didn't already have — raising RGB_OFFSET_MAX from 24px to 60px or slowing DECODE_SPEED from 1.7 to 1.0 changes how far or how long a pattern moves, not whether it moves under reduced motion, since the reduced-motion branch reads the same RENDER(p) function at a fixed p no matter which constant is in play. Accessibility here isn't a separate variant to turn on; it's the same function every variant already calls, just evaluated once instead of on a loop.

Where Can You Find a CSS Glitch Effect Generator Like This One?

A generator can hand you a working keyframe animation quickly, which is useful for a first draft, but every pattern here reads a JavaScript value on every animation frame instead of interpolating a CSS property — the only way pattern 04's brightness sort or pattern 09's per-character decode can work at all, since a generator built around CSS keyframes has no pixel buffer to sort and no character array to decode. What a generator also tends to skip is the reduced-motion branch and the keyboard handling covered above, both of which depend on reading a value once at load rather than only emitting a transition.

FAQ

Can I use this as a css glitch effect text animation for a headline?

Yes — pattern 09 is built exactly for that: point charX and POOL at a headline string instead of the demo text, and the same decode-progress value scrambles then resolves the new letters without touching RENDER(p) itself.

Why does pattern 05's noise look different from pattern 06's color drift?

Pattern 05 blends a random noise color into every pixel by a fading value, while pattern 06 only samples red, green, and blue from three different source coordinates — one adds a color that isn't in the image, the other only rearranges colors that already are.

Do any of the nine patterns need a library or a CDN script?

No — every pattern runs on getImageData, putImageData, and a seeded pseudo-random generator written in the same file, with no WebGL context and no external script tag anywhere in the nine demos.

Sources: MDN — getImageData · MDN — putImageData · MDN — requestAnimationFrame · MDN — prefers-reduced-motion · MDN — mix-blend-mode

Related reading: Image Reveal Effect CSS — 9 Canvas-computed Wipes · 9 CSS Screen Transition Animation Effects · 9 Perlin Noise Animations, No Libraries, Pure Math — the first shares this post's getImageData/putImageData spine but reveals one static image instead of switching between two screens, the second covers screen transitions built with CSS alone and no canvas pixel work, and the third's value-noise math is the same category of per-pixel randomness that pattern 05's noise burst uses over a much shorter duration.

Enter the archive password

The password is inside this article. You will find it as you read.