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

Pull to Refresh CSS: 9 Loading Patterns

One dragged distance value produces a progressively smaller displacement, one rotation value turns in proportion to that same ratio, and one commits only once

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

None of the nine ever reaches a real server — every one only simulates a network round trip with a local setTimeout, and 09's alternating outcome comes from an attempt counter rather than a real navigator.onLine check. All nine also answer the same three real pointer events: a visitor's pointerdown starts the pull, pointermove reports the raw drag distance on every frame, and pointerup decides whether that pull commits or eases back — every one of the nine wires up that real input through wireReal() alongside its own autoplay preview loop, not instead of it.

Anatomy of nine patterns — which value changes and when

Nine patterns split by what a released pull is actually allowed to do next, and that split is what keeps a pull to refresh animation collection from reading as nine identical spinners dropped under nine phone mockups.

Why two patterns shape a distance without ever triggering a refresh

01 and 04 never call a commit or a refresh at all — each one exists purely to shape how a raw dragged distance becomes a displayed one. 01's resist() runs the raw dragged distance through a Michaelis-Menten curve, D_MAX * raw / (raw + K), that saturates at D_MAX (56), so displacement keeps growing but by smaller and smaller amounts as raw climbs. 04's displayedOf() tracks raw almost one-to-one up to LINEAR (70px), then switches to an exponential term 1 - Math.exp(-excess / K) that creeps toward MAX_D (120) without ever reaching it. Both feed their result straight back through the same diff * 0.28/diff * 0.3 easing loop once the pointer releases, so the only thing that differs between them and the other seven is that there's no threshold to cross afterward.

Why two patterns skip the return-trip easing and reset the instant a finger lifts

02 and 06 are the only two of the nine with no settle()/goTo() loop for an uncommitted release — everywhere else, letting go below the threshold eases the display back over several frames. 02's onUp() calls apply(0) directly the moment a drag ends below ratio 1, so an uncommitted knob disappears in the same frame it was released rather than drifting back. 06's onUp() does the same by setting raw = 0; lastTick = 0; outright. What each one does instead with that saved complexity is spend it on the committed path: 02 hands off to spinFrame(), a genuinely time-based rotation keyed to elapsed milliseconds through SPIN_SPEED rather than to any dragged distance, and 06 layers a decaying burst AMP * Math.exp(-t / TAU) * Math.sin(t * W) on top of its position every time raw crosses a new TICK multiple.

Why the other five reach the same threshold decision through five different paths

03, 05, 07, 08, and 09 all check a released distance against a reference value and branch into a refresh — but no two of them compute that check the same way. 03's setCandidate() uses two thresholds instead of one, ON (100) and OFF (70), so a raw value hovering near 100 can't flicker the candidate flag on and off on every frame. 05 reuses its progress ratio directly: once raw >= REF (130) at release, commit() draws a checkmark stroke over a fixed 320ms rather than reusing the pull's own easing. 07 replaces easing entirely with a spring integration — accel = -K * (pos - target) - C * vel computed fresh every frame — so the marker actually overshoots the rest position and rebounds before settling. 08 and 09 both ease back through the familiar diff * 0.3 loop, but the interesting math happens after the refresh starts: 08 delays each row's reappearance by index * STAGGER_MS, and 09 decides success or failure from attemptCount % 2 rather than anything random at all.

Code per pattern

01Elastic pull — a refresh trigger whose resistance grows as the dragged distance value increases

As the dragged distance value grows, the actual displaced distance value increases by progressively smaller amounts, and once released that distance value returns to zero — useful for a mobile web feed refresh or the pull area at the top of a list.

Elastic pull — a refresh trigger whose resistance grows as the dragged distance value increases
Open in a new tab

01 · Elastic pull — a refresh trigger whose resistance grows as the dragged distance value increases — Measured area 15.385% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

D_MAXKresist

What the numbers say

Across 23 tracked frames, 01 moves on 14 of them, covering 15.39% cumulative area at a 30.6 average intensity — the visible change is the ghost trail, dot, and list all reading the same displayed value from resist(raw) at once, so all three slow their growth together as the drag lengthens.

  // Calculation layer: a resistance curve turning the raw dragged distance (raw)
  // into a displayed distance (displayed).
  // displayed = D_MAX * raw / (raw + K) — a saturating curve (Michaelis-Menten) where the
  // extension grows by smaller and smaller amounts as raw increases.
  var D_MAX = 56, K = 80, RAW_MAX = 200;

  function resist(raw) { return D_MAX * raw / (raw + K); }

  function apply(raw) {
    var displayed = resist(raw);
    var ratio = displayed / D_MAX;
    ghost.style.transform = 'translateY(' + Math.min(raw, D_MAX + 24).toFixed(1) + 'px)';
    ghost.style.opacity = String(Math.min(1, raw / 30));
    dot.style.transform = 'translateY(' + displayed.toFixed(1) + 'px) scale(' + (0.6 + 0.4 * ratio).toFixed(2) + ')';
    dot.style.opacity = String(ratio);
    list.style.transform = 'translateY(' + displayed.toFixed(1) + 'px)';
    hint.textContent = raw > 4 ? ('Pull ' + Math.round(raw) + 'px · Displaced ' + Math.round(displayed) + 'px') : 'Pull down to try';
  }

02Rotation bind — an indicator whose rotation value turns in proportion to the pulled distance ratio

The indicator's rotation value turns by the ratio of the pulled distance value to a reference distance, and once that ratio value reaches 1 it switches to a continuously spinning state — the kind of indicator a news feed or an inbox refresh puts right at the top.

Rotation bind — an indicator whose rotation value turns in proportion to the pulled distance ratio
Open in a new tab

02 · Rotation bind — an indicator whose rotation value turns in proportion to the pulled distance ratio — Measured area 14.129% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

SWEEPRADIUSspinFrame

What the numbers say

Across 23 tracked frames, 02 moves on 20 of them, covering 14.13% cumulative area at a 125.9 average intensity — the highest average intensity of the nine, since the knob sweeps a full 270-degree arc through place() on nearly every tracked frame instead of a smaller local displacement.

  function place(angleDeg, pullPx) {
    var rad = (angleDeg - 90) * Math.PI / 180;
    var x = Math.cos(rad) * RADIUS, y = Math.sin(rad) * RADIUS;
    knob.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)';
    list.style.transform = 'translateY(' + pullPx.toFixed(1) + 'px)';
  }

  var state = 'idle'; // idle -> dragging -> spinning -> idle
  var raw = 0, ratio = 0, spinAngle = 0, spinStart = 0, spinRaf = null;

  function fromRatio() { return ratio * SWEEP; }

  function spinFrame(ts) {
    if (!spinStart) { spinStart = ts; }
    var el = ts - spinStart;
    spinAngle = (el * SPIN_SPEED) % 360;
    place(spinAngle, 24);
    if (el < 900) {
      spinRaf = requestAnimationFrame(spinFrame);
    } else {
      state = 'idle'; ratio = 0; raw = 0; spinStart = 0;
      hint.textContent = 'Pull to refresh';
      place(0, 0);
    }
  }

03Threshold commit — a trigger that switches to a committed state once the pulled distance value passes a threshold

If the pulled distance value has passed a set threshold when released, it switches to a committed state and the refresh begins; if released while still below the threshold, the distance value returns to zero — the pattern behind a mailbox refresh or a social timeline refresh.

Threshold commit — a trigger that switches to a committed state once the pulled distance value passes a threshold
Open in a new tab

03 · Threshold commit — a trigger that switches to a committed state once the pulled distance value passes a threshold — Measured area 13.559% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

ONOFFsetCandidate

What the numbers say

Across 23 tracked frames, 03 moves on 21 of them, covering 13.56% cumulative area at a 35.0 average intensity — the disc's scale and position both track Math.min(1, raw / ON) continuously, so the visible growth keeps changing frame to frame even before the candidate flag flips.

  // Calculation layer: whether a pull becomes a committed candidate isn't decided by a single
  // comparison against raw — it's decided by hysteresis between two thresholds.
  // Crossing ON (100px) flips candidate to true; only dropping back below OFF (70px) flips it
  // false again, so the state never flickers near the boundary.
  var ON = 100, OFF = 70, MAX = 160;
  var candidate = false, state = 'idle'; // idle -> dragging -> committing -> idle

  function setCandidate(raw) {
    if (!candidate && raw >= ON) { candidate = true; }
    else if (candidate && raw < OFF) { candidate = false; }
  }

  function apply(raw) {
    setCandidate(raw);
    var pull = Math.min(raw, MAX) * 0.3;
    disc.style.transform = 'translateY(' + pull.toFixed(1) + 'px) scale(' + (0.7 + 0.3 * Math.min(1, raw / ON)).toFixed(2) + ')';
    disc.classList.toggle('prtc__disc--ready', candidate);
    list.style.transform = 'translateY(' + pull.toFixed(1) + 'px)';
    hint.textContent = candidate ? 'Release to refresh' : (raw > 4 ? 'Pull more (' + Math.round(raw) + '/' + ON + 'px)' : 'Pull down');
  }

04Rubber-band limit — a pull whose extension shrinks sharply once a maximum distance value is passed

As the pulled distance value nears a set maximum value, the extension value grows by a sharply smaller amount for the same input, and once released the distance value snaps back to its original position — a fit for a short list refresh or an area meant to prevent excessive pulling.

Rubber-band limit — a pull whose extension shrinks sharply once a maximum distance value is passed
Open in a new tab

04 · Rubber-band limit — a pull whose extension shrinks sharply once a maximum distance value is passed — Measured area 9.799% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

LINEARMAX_DdisplayedOf

What the numbers say

Across 23 tracked frames, 04 moves on 15 of them, covering 9.80% cumulative area at a 50.2 average intensity — the marker and list both read displayedOf(raw), which is why the marker keeps creeping toward the top of its track even as the list's own displacement visibly slows well before the marker does.

  // Calculation layer: raw tracks almost one-to-one up to LINEAR (70px), then an exponential
  // decay curve (1-e^-x/K) toward MAX_D (120px) makes the same additional force produce a
  // sharply smaller extension the closer raw gets to the cap.
  var LINEAR = 70, MAX_D = 120, K = 42, RAW_MAX = 260;
  var TRACK_H = 92, SCALE = TRACK_H / 150;

  function displayedOf(raw) {
    if (raw <= LINEAR) { return raw * 0.9; }
    var base = LINEAR * 0.9;
    var excess = raw - LINEAR;
    return base + (MAX_D - base) * (1 - Math.exp(-excess / K));
  }

  function apply(raw) {
    var displayed = displayedOf(raw);
    marker.style.transform = 'translateY(' + (-(displayed * SCALE)).toFixed(1) + 'px)';
    list.style.transform = 'translateY(' + (displayed * 0.55).toFixed(1) + 'px)';
    var pctOfCap = Math.round((displayed / MAX_D) * 100);
    hint.textContent = raw > 4 ? ('Displaced ' + Math.round(displayed) + 'px · of limit ' + pctOfCap + '%') : 'Pull down';
  }

05Icon progress ring — an indicator whose circular progress value fills with the pulled distance and morphs into a checkmark once complete

As the pulled distance value grows, the ring's progress value fills along its circumference, and once that progress value reaches full, a checkmark's stroke value draws inside the ring to signal that the refresh is complete — the kind of confirmation a to-do list refresh or loading the latest chat messages calls for.

Icon progress ring — an indicator whose circular progress value fills with the pulled distance and morphs into a checkmark once complete
Open in a new tab

05 · Icon progress ring — an indicator whose circular progress value fills with the pulled distance and morphs into a checkmark once complete — Measured area 1.133% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

RcheckLenringOf

What the numbers say

Across 23 tracked frames, 05 moves on 13 of them, covering 1.13% cumulative area at a 133.0 average intensity — the lowest cumulative area of the nine because only a thin ring stroke and a small checkmark ever repaint, yet each repainted pixel changes sharply, which is what pushes the average intensity this high.

  // Calculation layer: dividing the pull distance (raw) by REF gives a 0-1 progress value that
  // multiplies the ring's circumference (C) directly into strokeDashoffset — a fresh angle is
  // computed every frame, not a CSS transition.
  var REF = 130, MAX = 150;
  var R = 22, C = 2 * Math.PI * R;
  bar.style.strokeDasharray = C.toFixed(2);
  var checkLen = check.getTotalLength();
  check.style.strokeDasharray = checkLen.toFixed(2);
  check.style.strokeDashoffset = checkLen.toFixed(2);

  function ringOf(progress) { return C * (1 - progress); }

  function apply(raw) {
    var progress = Math.max(0, Math.min(1, raw / REF));
    bar.style.strokeDashoffset = ringOf(progress).toFixed(2);
    var pct = Math.round(progress * 100);
    hint.textContent = progress >= 1 ? 'Release to refresh' : (raw > 4 ? ('Pull progress ' + pct + '%') : 'Pull down');
  }

06Ratchet tick — an indicator that briefly snaps in position each time the pulled distance value passes a tick value

Each time the pulled distance value passes an evenly spaced tick value, the indicator's position value briefly jumps and settles, and this repeats as the count of passed ticks value increases — built for a refresh needing deliberate confirmation, or a list needing strong feedback.

Ratchet tick — an indicator that briefly snaps in position each time the pulled distance value passes a tick value
Open in a new tab

06 · Ratchet tick — an indicator that briefly snaps in position each time the pulled distance value passes a tick value — Measured area 8.078% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

TICKAMPtriggerBurst

What the numbers say

Across 23 tracked frames, 06 moves on 22 of them, the most of any of the nine, covering 8.08% cumulative area at a 74.2 average intensity — frame() runs on every animation frame regardless of whether a drag is active, which is why the marker is almost never still even between deliberate ticks.

  function triggerBurst(now) { burstActive = true; burstStart = now; }

  function frame(now) {
    var clamped = Math.min(raw, MAX_TICK);
    var tickIndex = Math.floor(clamped / TICK);
    if (tickIndex > lastTick) { triggerBurst(now); }
    lastTick = tickIndex;

    var basePos = (clamped / MAX_TICK) * TRACK_H;
    var burst = 0;
    if (burstActive) {
      var t = (now - burstStart) / 1000;
      burst = AMP * Math.exp(-t / TAU) * Math.sin(t * W);
      if (t > 0.6) { burstActive = false; }
    }
    marker.style.transform = 'translateY(' + (-(basePos) + burst).toFixed(1) + 'px)';
    list.style.transform = 'translateY(' + (clamped * 0.35).toFixed(1) + 'px)';
    hint.textContent = raw > 4 ? ('Tick ' + Math.min(tickIndex, N) + '/' + N) : 'Pull down';
    requestAnimationFrame(frame);
  }

07Spring settle — an indicator whose position value slightly overshoots the target after release before settling

Once released, the indicator's position value moves slightly past the fixed position value before decaying over time and settling at the fixed position value — a good match for a fixed top refresh indicator or a card-feed refresh.

Spring settle — an indicator whose position value slightly overshoots the target after release before settling
Open in a new tab

07 · Spring settle — an indicator whose position value slightly overshoots the target after release before settling — Measured area 7.859% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

KCaccel

What the numbers say

Across 23 tracked frames, 07 moves on 17 of them, covering 7.86% cumulative area at a 76.8 average intensity — the overshoot itself is what keeps the marker visibly moving for several extra frames after a released pull has already passed the point every other pattern here would call "done."

  var last = null;
  function frame(now) {
    if (last === null) { last = now; }
    var dt = Math.min(0.032, (now - last) / 1000);
    last = now;
    if (integrating) {
      var accel = -K * (pos - target) - C * vel;
      vel += accel * dt;
      pos += vel * dt;
      if (Math.abs(pos - target) < 0.5 && Math.abs(vel) < 5) {
        pos = target; vel = 0; integrating = false;
        if (pendingCommit) { pendingCommit = false; commit(); }
      }
    }
    render();
    requestAnimationFrame(frame);
  }

Every other pattern in this set eases with a plain diff * factor term recomputed from scratch each frame; 07 is the only one that integrates a velocity forward through accel = -K * (pos - target) - C * vel, which is what a pull to refresh animation needs if the release is meant to feel like it has real weight instead of just decaying toward zero.

08Row stagger reveal — an effect where each list row appears following an order value once the refresh finishes

Once the refresh reaches a finished state, each row's position value and opacity value return to normal after a delay proportional to its own order value, with higher order values appearing later — the effect behind a refreshed article list or a refreshed notification list.

Row stagger reveal — an effect where each list row appears following an order value once the refresh finishes
Open in a new tab

08 · Row stagger reveal — an effect where each list row appears following an order value once the refresh finishes — Measured area 8.638% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

STAGGER_MSLATENCY_MSrefreshCycle

What the numbers say

Across 23 tracked frames, 08 moves on 15 of them, covering 8.64% cumulative area at a 46.2 average intensity — this is the only one of the nine whose 측정.json shows autoplay-only motion at all (10 of its frames), because the row reveal only plays after a full refresh cycle completes, not on every partial drag.

  function refreshCycle() {
    disc.classList.add('prsg__disc--spin');
    hint.textContent = 'Refreshing';
    timers.push(setTimeout(function () {
      for (var i = 0; i < rows.length; i++) { rows[i].classList.remove('prsg__row--in'); }
      // Force a reflow so the browser actually applies the removed class before it gets added back.
      void list.offsetHeight;
      for (var j = 0; j < rows.length; j++) {
        (function (idx) {
          timers.push(setTimeout(function () { rows[idx].classList.add('prsg__row--in'); }, idx * STAGGER_MS));
        })(j);
      }
      var totalDelay = (rows.length - 1) * STAGGER_MS + 260;
      timers.push(setTimeout(function () {
        disc.classList.remove('prsg__disc--spin', 'prsg__disc--ready');
        hint.textContent = 'Pull down';
        raw = 0; apply(0);
      }, totalDelay));
    }, LATENCY_MS));
  }

09Failure shake — an effect where the indicator briefly shakes side to side once the refresh reaches a failed state

Once the refresh reaches a failed state, the indicator's horizontal position value oscillates several times over a short duration before returning to zero, and pulling again restarts the initial sequence — meant for signaling an unstable network state or a refresh that needs a retry.

Failure shake — an effect where the indicator briefly shakes side to side once the refresh reaches a failed state
Open in a new tab

09 · Failure shake — an effect where the indicator briefly shakes side to side once the refresh reaches a failed state — Measured area 2.943% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

attemptCountFREQshaking

What the numbers say

Across 23 tracked frames, 09 moves on 17 of them, covering 2.94% cumulative area at a 55.5 average intensity — the shake itself comes from a decaying sine AMP * Math.exp(-t / TAU) * Math.sin(t * FREQ) applied to the phone frame's own horizontal offset, and a high FREQ (46) is what keeps the oscillation tight and brief rather than a slow sway, which is why this stays one of the smaller cumulative-area patterns despite looking sharp.

  var attemptCount = 0, committing = false, commitTimer = null;
  function attempt() {
    attemptCount += 1;
    var fail = attemptCount % 2 === 1;
    committing = true;
    if (fail) {
      hint.textContent = 'Offline - retry';
      disc.classList.add('prof__disc--fail');
      shaking = true; shakeStart = performance.now();
      commitTimer = setTimeout(function () {
        disc.classList.remove('prof__disc--fail', 'prof__disc--ready');
        committing = false;
        raw = 0; apply(0);
        hint.textContent = 'Pull down';
      }, 640);

Where values reshape a pattern — distance, ratio, and threshold boundaries

Three constants decide how a pattern feels without changing what it does — swap them and the same drag still lands on the same end state, just along a different path, which is exactly what a pull to refresh animation needs to feel native on one product and cautious on another.

Why 03's ON decides how easily a pull commits, not how the disc looks mid-drag

setCandidate() only compares raw against ON; the disc's own scale and position, driven by Math.min(1, raw / ON), keep reading the same ON value regardless of where the actual ON/150 boundary sits. Raise ON from 100px to 150px and the disc still grows exactly the same way relative to that new target — what changes is how far a visitor must pull before letting go actually starts a refresh, so 150px cuts down on accidental refreshes from a light scroll bounce at the cost of a bigger deliberate gesture.

Why 07's K decides how tight the settle feels, not how far the marker travels

accel = -K * (pos - target) - C * vel uses K only to set spring stiffness; the released position always starts from wherever the pull left off, independent of K. At 170 the marker snaps back and overshoots only slightly before settling in a handful of frames; dropped to 80, the same release travels the same distance but the pull toward target is far weaker, so the marker drifts back more slowly and the overshoot itself grows more visible before it decays.

Why 08's STAGGER_MS decides how spread out the reveal feels, not how many rows appear

Every row still gets idx * STAGGER_MS as its own delay regardless of the constant's value — only the spacing between one row's reveal and the next changes. At 90ms the rows appear almost together, reading as a single reveal; stretched to 180ms, each row's appearance becomes distinct enough that a visitor can watch them arrive one at a time in sequence.

Where it breaks — fast pulls, short pulls, releasing midway

What happens on a rapid re-pull during an active refresh

03, 05, 06, 07, 08, and 09 all guard their onDown() with an early return while committing (or an equivalent state check) is true, so a second pull started mid-refresh is ignored outright until the current cycle finishes. 01 and 04 have no such guard because they have no refresh phase to protect — a rapid re-pull on either one just restarts the same resistance or overscroll curve from wherever raw currently sits. 02 falls in between: its guard only blocks a new drag while state === 'spinning', so a rapid re-pull during the brief window before that state is set can still interrupt an uncommitted knob.

What happens on a very short pull released well before the threshold

01, 03, 04, 05, 08, and 09 all ease a short pull back to zero over several frames through the same diff * factor loop, so a quick tap-and-release still visibly drifts back rather than vanishing. 02 and 06 do the opposite — apply(0) and raw = 0; lastTick = 0; run the instant the pointer lifts, so a released partial pull on either of these disappears in the same frame it ended rather than easing anywhere.

What happens if the drag races past the maximum before releasing

Every one of the nine clamps raw inside onMove() against RAW_MAX or an equivalent MAX cap, so a drag that keeps moving well past the visible track never feeds an unbounded number into resist(), displayedOf(), or any of the ratio calculations. 03's hysteresis adds one more detail here: because OFF (70) sits below ON (100), a fast drag that shoots straight past both values in a single pointermove still flips candidate to true on that frame, so speed alone can't skip the commit the way it might if the check only fired on a slow crawl through the boundary.

Where this set differs from Infinite Scroll Loading: 9 Essential UI Patterns, 9 Loading Screen Designs — Pretty CSS Loaders, and 9 Swipe to Delete List UI Patterns (Copy-Paste) is which value each set actually recomputes from. The infinite-scroll set derives its nine values from a continuously changing scroll position — remaining scroll distance, added height offset, visible index range — that never resolves into a released, thresholded decision the way a pull does. The loading-screen set never touches a pointer value at all; its nine loaders run on a fixed CSS keyframe cycle regardless of anything a visitor does. The swipe-list set already folds one pull-to-refresh pattern into a broader nine-pattern collection alongside swipe-to-delete and pagination, where here all nine specialize in that single gesture instead, comparing a resistance curve against a hysteresis threshold against a spring integration. All nine source files behind this set, unlocked with the password bffdpzhj, drop straight into a real pull-to-refresh once the simulated network delay is swapped for an actual fetch.

Three variants and accessibility

Only three of these nine pull to refresh css patterns expose a constant worth turning into a shipped variant — not because the other six lack tunable numbers, but because ON, K, and STAGGER_MS are the three where a single value change most visibly reshapes how forgiving, weighty, or spread out the interaction feels.

Variant What changes What it trades off
03's ON from 100px to 150px The pull needs to travel further before it becomes a committed candidate At 100px a light pull is already enough to arm the refresh, ready to fire the moment a finger lifts; at 150px a visitor must commit to a longer, more deliberate pull, trading a quicker refresh for fewer accidental ones
07's K from 170 to 80 The marker settles into place more loosely after release At 170 the spring is stiff and the marker snaps to rest within a handful of frames; at 80 the same release drifts back more slowly with a more visible overshoot, reading as looser and less mechanical
08's STAGGER_MS from 90ms to 180ms Each row's reappearance becomes more distinct from the next At 90ms the rows arrive almost as one block; at 180ms each row's entrance is spaced far enough apart that a visitor can watch them appear one after another

All nine check window.matchMedia('(prefers-reduced-motion: reduce)').matches before starting their own autoplay preview loop — the guard only gates that setInterval demo tick, so a real pull through wireReal()'s pointerdown/pointermove/pointerup chain always plays its full resistance curve, rotation, threshold, or spring settle regardless of that setting. None of the nine sets an aria-label, aria-hidden, or an aria-live region anywhere in its markup — each only swaps the hint span's own text content between states like "Pull down," "Release to refresh," and "Refreshing," so without a live region wired to that span a screen reader has no automatic way to announce the change on its own.

FAQ

Are these pull to refresh patterns built with any animation library?

No — all nine run on plain requestAnimationFrame, setTimeout, and CSS transforms with zero external dependencies, so pasting the HTML, SCSS, and JS in as-is is enough to see the same motion running locally.

Do any of the nine call a real server or check real connectivity?

No — every pull is simulated with a local setTimeout, and 09's failure state comes from an attemptCount % 2 alternation rather than navigator.onLine or any real network check. Nothing in the set calls fetch.

Is a swipe to refresh animation on a horizontal list built the same way as these nine?

Not quite — these nine all read a vertical pointermove distance against a phone-style container, while a horizontal swipe pattern tracks the same three pointer events along the other axis and usually reveals or removes a row instead of triggering a refresh. The threshold and easing math in 03, 07, and 08 carries over directly; only the axis and the end action change.

Sources: MDN — Pointer events · MDN — transform-function: translate() · MDN — Window.requestAnimationFrame · MDN — prefers-reduced-motion

Enter the archive password

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