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

QR Code Loading Animation — 9 CSS Expiry Patterns

Not a scan failure, not sitting too long — one time left ratio for qr code loading animation must run out: item 3 shrinks a ring, item 4 carves an angle, item

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

How One Time-left Value Moves Through Nine QR Loading Screens

Every grid on this page is a fixed-seed pseudorandom dot pattern built by a small linear congruential generator (qrRand or seedRand, seeded once per item) — it looks like a QR module layout but encodes nothing and can't be scanned by a real reader; the point of each demo is the one number moving the dots, rings, or digits, not the code payload. Six of the nine — 01, 02, 05, 06, 07, and 09 — read a progress-style value that climbs or steps as work finishes: a fill order, a scan-line distance, a recognition pop, a settle ratio, a digit roll, a carousel index. The other three — 03, 04, and 08 — read the same kind of value counting the opposite way, a time left ratio that starts at 1 and runs out, which is what the hook above is describing: item 3 narrows a ring by it, item 4 turns it into an angle, item 8 waits for it to hit zero before it shakes.

01QR Code Whose Modules Fill In With the Progress Value

As the generation progress value fills from 0 to 1, the grid's dark modules appear one at a time instead of snapping in all together — each module's reveal delay comes from buildModules' row position plus a small qrRand-seeded jitter, scaled by QR_MODULE_REVEAL_STAGGER_MS, so the wave moves top to bottom without the value ever encoding anything real.

QR Code Whose Modules Fill In With the Progress Value
Open in a new tab

01 · QR Code Whose Modules Fill In With the Progress Value — Measured area 17.923% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

QR_MODULE_REVEAL_STAGGER_MSbuildModulesqrRand
  // One calculated layer: take each module's row (y) value as the stagger baseline, then
  // nudge it with a seeded random offset so the reveal wave spreads top to bottom naturally (not real encoding).
  var jitterRand = qrRand(20260924);
  var order = [];
  for (var y2 = 0; y2 < SIZE; y2 += 1) {
    for (var x2 = 0; x2 < SIZE; x2 += 1) {
      var jitter = (jitterRand() - 0.5) * QR_MODULE_REVEAL_STAGGER_MS;
      order.push({ x: x2, y: y2, delay: y2 * QR_MODULE_REVEAL_STAGGER_MS + jitter });
    }
  }
  var maxDelay = 0;
  order.forEach(function (o) { if (o.delay > maxDelay) maxDelay = o.delay; });

That single QR_MODULE_REVEAL_STAGGER_MS number is the only knob here: raise it and each row's modules spread further apart in time, lower it and the whole grid fills almost as one block.

Measured: cumulative area 17.9% of the frame, average intensity 83.8, moved 9/23 frames.

02QR Code Whose Corner Frame Blinks While Waiting to Be Read

A scan-line value sweeps down the frame over SWEEP_MS, and each corner's glow strength comes from how close that line currently is to it — cornerGlow turns that distance into a 0-to-1 value clamped at both edges, scaled by SCF_CORNER_GLOW_RANGE_PX, so the four marks brighten and dim in turn instead of holding one fixed brightness.

QR Code Whose Corner Frame Blinks While Waiting to Be Read
Open in a new tab

02 · QR Code Whose Corner Frame Blinks While Waiting to Be Read — Measured area 28.194% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCF_CORNER_GLOW_RANGE_PXcornerGlowseedRand
  function cornerGlow(dist) {
    // One calculated layer: distance to the scan line becomes a 0-1 corner glow strength, zero once outside the range.
    var p = 1 - dist / SCF_CORNER_GLOW_RANGE_PX;
    return p < 0 ? 0 : (p > 1 ? 1 : p);
  }

  function draw(elapsed) {
    var cw = canvas.width, ch = canvas.height;
    ctx.clearRect(0, 0, cw, ch);

Every corner reads the same cornerGlow(dist) output, so the four marks always brighten and dim together rather than drifting out of sync with each other.

Measured: cumulative area 28.2% of the frame, average intensity 62.6, moved 23/23 frames.

03QR Code Whose Pulse Ring Narrows by the Time Left Value

A ring is born every PULSE_RING_INTERVAL_MS, and ringsFor returns the age-to-life ratio of every ring still alive at the current instant to set each ring's radius up to MAX_RADIUS — as the time left ratio drops, fewer rings stay young enough to read as wide, so the whole cluster visibly tightens instead of one ring alone shrinking.

QR Code Whose Pulse Ring Narrows by the Time Left Value
Open in a new tab

03 · QR Code Whose Pulse Ring Narrows by the Time Left Value — Measured area 40.212% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PULSE_RING_INTERVAL_MSringsForMAX_RADIUS
  function ringsFor(elapsed) {
    var out = [];
    var firstIndex = Math.ceil((elapsed - RING_LIFE_MS) / PULSE_RING_INTERVAL_MS);
    var lastIndex = Math.floor(elapsed / PULSE_RING_INTERVAL_MS);
    for (var i = Math.max(0, firstIndex); i <= lastIndex; i += 1) {
      var born = i * PULSE_RING_INTERVAL_MS;
      var age = elapsed - born;
      if (age < 0 || age > RING_LIFE_MS) continue;
      out.push(age / RING_LIFE_MS);
    }
    return out;
  }

ringsFor never looks at a wall clock — it only compares each ring's birth time against the current elapsed value, so the narrowing is a property of the ratio, not of real time passing.

Measured: cumulative area 40.2% of the frame, average intensity 46.9, moved 23/23 frames.

04Countdown Ring Whose Time Left Value Becomes an Angle Value

The same time left ratio from the hook above is converted straight into an angle — ratio * 360 — that erodes a circular ring clockwise until nothing is left once the value hits zero, and under 20% remaining an urgency value blends the ring color from blue to red and brightens the card's background so the last stretch reads as more alarming than the first.

Countdown Ring Whose Time Left Value Becomes an Angle Value
Open in a new tab

04 · Countdown Ring Whose Time Left Value Becomes an Angle Value — Measured area 61.73% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

QR_EXPIRE_DURATION_MSDEMO_ACCEL_FACTORurgency
    // One calculated layer: elapsed ratio becomes a 0-360 angle that erodes the ring, and once
    // under 20% remaining, interpolates the ring color from blue to red. The same urgency value
    // also reaches the background tone and the ring's outer glow, so a wide area shifts together.
    var ratio = Math.min(1, virtualElapsed / QR_EXPIRE_DURATION_MS);
    var remainRatio = 1 - ratio;
    var angle = ratio * 360;
    var urgency = remainRatio < 0.2 ? (0.2 - remainRatio) / 0.2 : 0;
    var r1 = lerp(120, 255, urgency), g1 = lerp(200, 90, urgency), b1 = lerp(255, 90, urgency);
    var ringColor = 'rgb(' + Math.round(r1) + ',' + Math.round(g1) + ',' + Math.round(b1) + ')';

The demo compresses QR_EXPIRE_DURATION_MS — genuinely 30000ms, the same constant a production countdown would use — with a demo-only DEMO_ACCEL_FACTOR so the loop plays out in a couple of seconds here, without the constant itself changing.

Measured: cumulative area 61.73% of the frame, average intensity 81.0, moved 23/23 frames.

05QR Code That Folds Into a Checkmark the Instant It Is Read

The instant a scan is recognized, every module's position value converges at once into a checkmark-shaped layout instead of fading or sliding in gradually, and springScale overshoots past 1 by SUCCESS_CHECK_SPRING_OVERSHOOT before settling so the pop has a small bounce rather than landing flat.

QR Code That Folds Into a Checkmark the Instant It Is Read
Open in a new tab

05 · QR Code That Folds Into a Checkmark the Instant It Is Read — Measured area 6.892% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

SUCCESS_CHECK_SPRING_OVERSHOOTspringScalePOP_START
  function springScale(p, overshoot) {
    if (p <= 0) return 0;
    if (p >= 1) return 1;
    var decay = Math.exp(-4.5 * p);
    return 1 + overshoot * decay * Math.cos(p * Math.PI * 2.2);
  }

  function clamp01(v) { return v < 0 ? 0 : (v > 1 ? 1 : v); }

Because springScale is one reusable function of progress and overshoot, the same bounce shape drives both the module convergence and the checkmark stroke without two separate easing curves to keep in sync.

Measured: cumulative area 6.9% of the frame, average intensity 119.7, moved 14/23 frames.

06QR Code That Settles as Its Jitter Amplitude Shrinks While Generating

As the generation progress value grows, each module's jitter amplitude decays exponentially from QR_NOISE_SETTLE_AMPLITUDE_PX toward zero over SETTLE_MS so a grid that starts trembling like noise gradually settles into a fixed layout, and because every module keeps its own seeded phase and direction, the settle never reads as one uniform wobble shrinking — each dot calms slightly out of step with its neighbors.

QR Code That Settles as Its Jitter Amplitude Shrinks While Generating
Open in a new tab

06 · QR Code That Settles as Its Jitter Amplitude Shrinks While Generating — Measured area 10.958% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

QR_NOISE_SETTLE_AMPLITUDE_PXSETTLE_MSwobble
    // One calculated layer: amplitude decays exponentially over time — starting at
    // QR_NOISE_SETTLE_AMPLITUDE_PX and fading to 0 over SETTLE_MS, with each module wobbling
    // at its own phase and direction as it settles into place.
    var settleP = Math.min(1, elapsed / SETTLE_MS);
    var amp = QR_NOISE_SETTLE_AMPLITUDE_PX * Math.exp(-4 * settleP);

    ctx.fillStyle = 'rgb(255,247,230)';
    modules.forEach(function (m) {
      var wobble = Math.sin(elapsed / 1000 * m.freq + m.phase) * amp;
      var jx = Math.cos(m.dir) * wobble;
      var jy = Math.sin(m.dir) * wobble;
      var s = cell * 0.68;
      var cx = ox + m.x * cell + cell / 2 + jx;
      var cy = oy + m.y * cell + cell / 2 + jy;
      ctx.globalAlpha = 0.55 + 0.45 * settleP;
      ctx.fillRect(cx - s / 2, cy - s / 2, s, s);
    });

Only one number, the starting amplitude, decides how rough the noise looks at the very first frame — the decay curve and timing stay fixed.

Measured: cumulative area 11.0% of the frame, average intensity 44.7, moved 23/23 frames.

07QR Code Panel Whose Payment Amount Digits Roll Over

Whenever the demo's placeholder amount changes, each digit's roll progress comes from progressFor, which subtracts a per-digit stagger delay before dividing by ROLL_MS so digits further from the last place start rolling slightly later than the ones next to them, and easeOutBack shapes that progress into a small overshoot so each digit settles with a tiny bounce instead of stopping dead.

QR Code Panel Whose Payment Amount Digits Roll Over
Open in a new tab

07 · QR Code Panel Whose Payment Amount Digits Roll Over — Measured area 11.454% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

AMOUNT_ROLL_DIGIT_STAGGER_MSeaseOutBackROLL_MS
  function easeOutBack(t) {
    var c1 = 1.4, c3 = c1 + 1;
    return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
  }

  function progressFor(elapsed, segStart, delay) {
    var local = elapsed - segStart - delay;
    if (local <= 0) return 0;
    if (local >= ROLL_MS) return 1;
    return local / ROLL_MS;
  }

AMOUNT_ROLL_DIGIT_STAGGER_MS is the only value deciding how staggered the digits look — raise it and the roll reads left to right, lower it toward zero and every digit rolls at once.

Measured: cumulative area 11.5% of the frame, average intensity 74.0, moved 20/23 frames.

08QR Code That Shakes the Instant the Time Left Value Hits Zero

The instant the time left ratio hits zero, shakeOffset spikes a decaying sine wave across SHAKE_MS, driven by SHAKE_FREQ and capped at EXPIRED_SHAKE_AMPLITUDE_PX, then fades to nothing as a reissue icon appears — the demo's autoplay loop schedules two expiries inside one cycle, so the shake-then-retry sequence is visible without waiting through the full real duration twice.

QR Code That Shakes the Instant the Time Left Value Hits Zero
Open in a new tab

08 · QR Code That Shakes the Instant the Time Left Value Hits Zero — Measured area 32.133% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EXPIRED_SHAKE_AMPLITUDE_PXshakeOffsetSHAKE_FREQ
  // One calculated layer: a decaying sine wave — frequency (SHAKE_FREQ) times exponential decay — produces the shake.
  function shakeOffset(t) {
    if (t < 0 || t > SHAKE_MS) return 0;
    var p = t / SHAKE_MS;
    return Math.sin(p * Math.PI * 2 * SHAKE_FREQ) * EXPIRED_SHAKE_AMPLITUDE_PX * Math.exp(-3.2 * p);
  }

  // Autoplay expiry windows: first expiry (EXPIRE_AT to RETRY_AT), second expiry (EXPIRE_AT2 to loop end).
  function autoExpired(elapsed) {
    return (elapsed >= EXPIRE_AT && elapsed < RETRY_AT) || elapsed >= EXPIRE_AT2;
  }
  function autoExpireStart(elapsed) {
    return elapsed < RETRY_AT ? EXPIRE_AT : EXPIRE_AT2;
  }

shakeOffset only reads elapsed time since the expiry started, so the same function produces both autoplay expiries and a manual retry click without any duplicated shake logic.

Measured: cumulative area 32.1% of the frame, average intensity 65.1, moved 23/23 frames.

09QR Carousel That Advances Codes by an Index Value

Coupon, point, and payment codes each map to one of five index values, and easeInOut shapes the fractional step between two indexes into a perspective slide instead of a linear one, with every code's scale, opacity, and horizontal offset all read from that same distance-from-center value so the whole row moves as one coordinated deck rather than five independent sprites.

QR Carousel That Advances Codes by an Index Value
Open in a new tab

09 · QR Carousel That Advances Codes by an Index Value — Measured area 18.378% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

MULTI_CODE_CAROUSEL_STEP_MSeaseInOutgrids
    var raw = elapsed / MULTI_CODE_CAROUSEL_STEP_MS;
    var index = Math.floor(raw) % COUNT;
    var localT = raw - Math.floor(raw);
    var eased = easeInOut(localT);
    var pos = index + eased;

    for (var i = 0; i < COUNT; i += 1) {
      // One calculated layer: distance from the current position drives perspective scale, opacity, and horizontal offset together.
      var diff = i - pos;
      while (diff > COUNT / 2) diff -= COUNT;
      while (diff < -COUNT / 2) diff += COUNT;
      var dist = Math.abs(diff);
      if (dist > 1.8) continue;
      var scale = Math.max(0.35, 1 - dist * 0.42);
      var alpha = Math.max(0, 1 - dist * 0.55);
      var cx = cw / 2 + diff * stepX;

MULTI_CODE_CAROUSEL_STEP_MS is the only timing value in the loop — shorten it and all five codes cycle faster without the perspective math changing at all.

Measured: cumulative area 18.4% of the frame, average intensity 118.8, moved 23/23 frames.

Where the Value Drives a Fill Order, a Countdown, or an Index

Six of these nine — the scan and generation math behind 01, 02, 05, 06, 07, and 09 — read a value climbing toward 1 as work finishes, which is the same shape worth wanting if one loader here got pulled out as a css loading animation codepen snippet on its own: drop in a single progress number and the rest follows. The other three, 03, 04, and 08, invert that shape into a countdown that starts at 1 and runs out, which reads closer to an expiry timer than a loading bar even though the visual vocabulary — rings, angles, color — looks the same across both groups. Item 09 sits apart from either: its index value doesn't climb or count down at all, it just steps between five fixed positions, so the carousel reads as switching rather than finishing.

Where These Nine Patterns Break on Reduced Motion and Older Devices

Every one of the nine checks prefers-reduced-motion before its first frame and, if it's set, jumps straight to the pattern's resting value instead of animating into it — nobody who has asked for less motion ever sees the wave, the countdown, or the shake play out, only the finished state. The one real trap worth knowing before copying any of this: QR_EXPIRE_DURATION_MS in item 4 is genuinely 30000ms, the same constant a production countdown would use — only the demo's DEMO_ACCEL_FACTOR speeds up how fast elapsed time accumulates so the loop finishes in a couple of seconds on this page, and that multiplier is the one piece removed before anything ships, not the constant itself. None of the nine reads a real clock, a real payment amount, or a real URL anywhere — the numbers in item 7's panel and every QR grid on this page are placeholders, and getting the actual files out of the zip needs the wkt849wy written a little further down this page.

Accessibility: What prefers-reduced-motion Actually Removes Here

The same media query check sits at the top of every one of the nine index.html files, and each one responds the same way: set playing to false so the requestAnimationFrame loop never starts, remove the is-demo class so any CSS-only decorative motion stops too, and render the pattern's finished value in a single frame instead of the frames in between.

Item What's removed under reduced-motion
01 Staggered reveal wave — grid renders with every dark module already filled
04 Countdown erosion — ring renders fully depleted at its most urgent color
08 Expiry shake — card renders already shaken out, retry icon visible
09 Perspective slide — carousel renders on its first code, centered and still

What Nine QR Loading Screens Actually Moved, Frame by Frame

Every row below comes straight from this run's 측정.json files, one per item, with moved frames counted out of 23 possible deltas between the 24 captured frames.

Item Cumulative area Avg. intensity Moved frames
01 Module reveal grid 17.9% 83.8 9/23
02 Corner frame blink 28.2% 62.6 23/23
03 Pulse ring expiry 40.2% 46.9 23/23
04 Countdown ring 61.7% 81.0 23/23
05 Scan success checkmark 6.9% 119.7 14/23
06 Generate noise settle 11.0% 44.7 23/23
07 Amount roll swap 11.5% 74.0 20/23
08 Expired shake retry 32.1% 65.1 23/23
09 Multi-code carousel 18.4% 118.8 23/23

Where a Time-left QR Loader Actually Earns Its Place in a Real Screen

A payment or check-in QR code that's about to expire is the one screen where a shrinking value genuinely needs to stay visible, not decorative — items 3, 4, and 8 together (ring, angle, shake) give a user three separate cues for the same countdown before it's too late to notice. A generation-in-progress screen, the moment right after a code is requested but before encoding has finished, is where items 1, 2, and 6 fit — filling, scanning, and settling are all ways of saying "not yet" without a blank spinner. A ticket or coupon wallet with more than one code queued up is where item 9's index-driven carousel belongs, since swapping between five fixed codes is a different problem from counting any one of them down.

Three Constants Worth Bending First

Constant Change What shifts
QR_MODULE_REVEAL_STAGGER_MS 18 → 36 Item 1's reveal wave spreads out to where each module fills visibly on its own, instead of the grid nearly flooding in at once
QR_EXPIRE_DURATION_MS 30000 → 15000 Item 4's countdown ring erodes twice as fast, reading as urgent instead of unhurried
QR_NOISE_SETTLE_AMPLITUDE_PX 6 → 2 Item 6's generation jitter stays mild and reads as stable from the first frame instead of settling out of a noisy start

Where Else Can You Find css loading animation examples for QR Codes?

9 CSS Loading Animation Patterns, Canvas Physics covers loaders where a value drives real physics — gravity, springs, collisions — rather than a QR-shaped grid, which is the harness these nine borrow their spring and decay math from. CSS 3D Cube Animation Loaders, 9 Patterns takes the same one-value-drives-nine-patterns idea into 3D space instead of a flat canvas, useful if a css loading animation template needs depth rather than a countdown. AI Loading Animation: 9 Patterns by One Value is the closest sibling in spirit — an indeterminate progress value standing in for work with no fixed duration — where this set's countdown items 3, 4, and 8 instead have a duration that's known in advance and simply runs out. For the underlying API these demos lean on for their per-frame timing, MDN's requestAnimationFrame reference documents the callback loop all nine tick functions above are built on.

Questions Worth Answering Before You Wire Up a Time-left QR Loader

Are these QR grids real, scannable codes?

No — every grid on this page is a fixed-seed pseudorandom dot pattern from qrRand or seedRand, built to look like a QR module layout; it encodes nothing and no camera will read it, which is why that honesty note sits right in the code comment for item 1.

Does the 30-second countdown in item 4 actually take 30 seconds to watch?

Not on this page — QR_EXPIRE_DURATION_MS really is 30000ms, but a demo-only DEMO_ACCEL_FACTOR speeds up how fast that elapsed time accumulates so the loop finishes in a couple of seconds here; the multiplier is removed before the constant ships anywhere real.

Can I use one of these nine as a css loading animation generator input instead of hand-editing the constants?

Yes — every constant named above (QR_MODULE_REVEAL_STAGGER_MS, QR_EXPIRE_DURATION_MS, QR_NOISE_SETTLE_AMPLITUDE_PX, and the rest) sits at the top of its own index.html with nothing else to touch, so a generator only needs to swap that one number in per pattern rather than rewrite any function beneath it.

Enter the archive password

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