Skip to content
animorio
IT
All components

Pull to refresh

Loading, Physics, TransitionsGSAPAdvancedCompressed weight: Demo 8.1 KBGSAP 28 KBFreeDownload the zip

A list of requests that refreshes when pulled down: it follows the hand on a rubber band that gets harder the further it goes, the rows draw a little apart, a ring fills and past the mark its arrow turns round; let go there, the ring spins, closes into a tick and the rows go back up in a wave while the new request drops into the space.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pull to refresh</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/golos-text@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- pull to refresh: the list pulled down stretches like an elastic and a ring fills; let go past the mark and it
         refreshes, then the rows go back up in a wave and the new request drops into the space they leave -->
    <section class="inbox" aria-labelledby="inbox-title" data-inbox data-autoclic>
      <header class="inbox__head">
        <div>
          <h2 class="inbox__title" id="inbox-title">Requests</h2>
          <p class="inbox__when" data-when>Updated 5 min ago</p>
        </div>
        <button class="inbox__refresh" type="button" aria-label="Refresh" data-refresh>
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3"/><path d="M19.6 4.2v3.6H16"/></svg>
        </button>
      </header>
      <div class="inbox__body">
        <div class="ptr" aria-hidden="true" data-ptr>
          <svg class="ptr__ring" viewBox="0 0 40 40" data-ring>
            <circle class="ptr__track" cx="20" cy="20" r="16"/>
            <g data-spin>
              <circle class="ptr__arc" cx="20" cy="20" r="16" transform="rotate(-90 20 20)" data-arc/>
            </g>
            <path class="ptr__arrow" d="M20 13.5v13M15 21.5l5 5 5-5" data-arrow/>
            <path class="ptr__check" d="M14 20.5l4 4 8-8.5" data-check/>
          </svg>
          <span class="ptr__label" data-label>Pull to refresh</span>
        </div>
        <div class="inbox__scroll" data-scroll>
          <ul class="inbox__list" aria-labelledby="inbox-title" data-list></ul>
        </div>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

25 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “Pull to refresh” from Animorio and fit it into my site

What it does

On a black background a dark box with Requests, Updated 5 min ago and a round button to refresh; under it, six requests on dark cards, each with its coloured circle holding the initials, the name, the message and how long ago (Hannah Brooks, Leo Marchetti, Sara Lindqvist, Omar Haddad, Chloé Martin, Ben Okafor). Pulling the list down from the top, with a finger, the mouse or a trackpad, the list follows on a rubber band that gets harder as it goes, the rows draw a little apart and behind them a ring shows, filling with coral, with an arrow and Pull to refresh; past the mark the ring gives a little hop, the arrow turns up and the words become Release to refresh. Letting go there, the list stays a little lower while the ring spins, lengthening and shortening (Refreshing), then it closes and a tick draws inside it (Up to date); the rows go back up in a wave, each on its own spring, and the new request drops in from the top, edged in coral with the label New, while above it reads Updated just now. Letting go before the mark, the list goes back up without refreshing. The round button does the same, its arrow turning until it is done.

How it works

The rubber band

The hand’s distance becomes the list’s movement with STRETCH × (1 − e^(−distance ÷ STRETCH)): at first it follows almost the same, then less and less, and it never goes past STRETCH pixels. It is pulled only when the list is at its top, with a finger, the mouse or a trackpad’s wheel (while the wheel keeps going; when it stops, it is like letting go); otherwise the list scrolls as usual.

Rows on springs

The first row follows the hand exactly (with the wheel, which moves in jumps, it is on a spring too); each of the others heads for its place on a spring (SPRING), worked out on GSAP’s ticker in steps of 4 milliseconds, each a little softer than the one above (LAG): so, let go, the list goes back up in a wave. The places spread by up to SPREAD pixels a row towards the rubber band’s limit, and a row never goes over the one before it: when it catches up, it is pushed along.

The ring

Behind the list, an SVG circle whose arc (stroke-dasharray) is the share of the mark (MARK) reached, while the ring grows from 0.62 to 1; past the mark the arrow turns 180 degrees with back.out and the ring gives a hop. While refreshing, the arc spins, lengthening and shortening in a loop; at the end it closes, finishes its turn instead of stopping where it is, and the tick draws inside it with stroke-dashoffset.

The new request

Added at the top, it moves the others down in the layout: their springs take that move off at once, so they stay where they were and then slide down by themselves; the new one starts above, hidden under the head, and drops into place. A moment later it loses its edge and the label New. There are eight requests at most: when one arrives, the oldest goes.

Keys and the preview

The round button refreshes with the keys too (first it takes the list to the top, if it had scrolled); the list has aria-busy while it refreshes, and an aria-live region says Refreshing and then who the new request came from. A click from a script (the library’s preview) is a pretend finger: it pulls the list past the mark, holds a moment and lets go.

Values to change

NameFileDefaultWhat it changes
MARKscript.js84How far, in pixels, the list must be pulled for letting go to refresh it.
STRETCHscript.js170The most the list can be pulled, in pixels: the rubber band’s limit.
SPREADscript.js10How far, in pixels, each row draws away from the one above, towards the limit.
SPRINGscript.js[380, 0.74]The rows’ springs: how stiff they are and how much they are held back.
LAGscript.js0.35How much softer each spring is than the one above: the wave.
LOADscript.js1.3The seconds a refresh lasts (here a pretend one).
ARRIVALSscript.js[['Ines Duarte', 'Table for 2 tonight at 9, if anything is free?'], …]The requests that arrive with a refresh, one each time.

Try them in the Playground

When to use it

For lists that change by themselves and are checked often on a phone: messages, bookings, orders, news, notifications, a notice board.

On phones

It is pulled with a finger: on the list at its top a drag downwards does not scroll the page (the touchmove is stopped only then), while in every other case the list scrolls as usual, without bouncing past its edge (overscroll-behavior contain).

With reduced motion

The list still follows the hand, but without springs or wave: let go, it goes straight to its place; the ring and the button’s arrow do not spin (the ring is full while refreshing), the tick appears without being drawn and the new request appears already in place.

Pitfalls

  • A row added at the top moves all the others in the layout: to make them slide, the move is taken off their springs at the same moment (they stay where they were) and the springs are left to make it up.
  • On a phone the drag downwards on the list at its top must be stopped (touchmove with passive false and preventDefault), or Safari bounces the page; only then, so normal scrolling stays as it is.
  • A line with round ends and a dash of no length (stroke-dashoffset equal to the length) still leaves a speck: the tick stays hidden until it is drawn.
  • In a timeline that swaps a word (it leaves, the text changes, it comes in) the entrance’s fromTo needs immediateRender false: otherwise GSAP applies it at once and the old word disappears without leaving.

Libraries

GSAP 3.15.0, Golos Text (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 8.1 KB; the libraries: GSAP 28 KB.

Testing

Tested on 3 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components