v1.0.5  ·  MIT  ·  Zero deps

Pull to Refresh
done right.

The smallest, cleanest pull-to-refresh for the web. No framework needed. Plug in, configure one callback, ship.

0 dependencies
~3KB gzipped
3 lines of setup
Pull down to refresh
Activity Feed LIVE
🏋️
Workout logged
2 minutes ago
🔥
7-day streak!
1 hour ago
📊
Weekly report ready
Yesterday
🎯
Goal achieved
Monday
💪
New PR: Bench 100kg
Tuesday

Try it yourself.

Grab the handle and pull down. Works with both mouse and touch on any device.

Notifications

Grab the handle & pull down

Silky smooth,
zero config.

The indigo progress bar fills as you drag. When it hits 100% and you release, your async callback fires. The spinner waits for your Promise to resolve — then resets automatically.

1
Grab the grip
The small indigo handle at the very top of the container is your drag target.
2
Pull until full
The progress bar fills as you drag. The label flips to "¡Suelta!" when you hit the threshold.
3
Release & reload
Drop it. The spinner spins while your onRefresh promise resolves.

Everything you need.
Nothing you don't.

A focused library that does one thing and does it perfectly.

Zero dependencies
Pure vanilla JavaScript. No React, Vue, or build step. One file that works on any page.
🎨
Auto-styled UI
Progress bar, grip handle, and spinner are injected automatically. Looks great out of the box.
📱
Touch & mouse
Native touch events for mobile and mouse drag for desktop — both work seamlessly.
Async / Promise
Pass an async function. PullyJS shows the spinner until your Promise resolves or rejects.
🔌
Universal module
Ships as UMD. Use it with a <script> tag, require(), or import.
🗑️
Clean destroy()
One method removes every event listener and DOM node. No memory leaks. Ever.

Dead simple API.

One constructor. Three options. One method to clean up.

new Pully(container, options)
Attach pull-to-refresh to any element. container is a CSS selector or HTMLElement.
onRefresh: async () => void
Called after the user releases past the threshold. Awaited before spinner resets.
onRelease: () => void
Fires immediately on release, before the async refresh starts.
threshold: 100
Pixels the user must drag before a release triggers refresh. Default: 100.
instance.destroy()
Removes all event listeners and injected DOM elements. Good for SPA cleanup.
app.js
 1// ① Import (or use a <script> tag)
 2import Pully from 'pullyjs';
 3
 4// ② Initialize
 5const pully = new Pully('#feed', {
 6  // required: your refresh logic
 7  onRefresh: async () => {
 8    const posts = await fetchLatest();
 9    renderFeed(posts);
10  },
11
12  // optional: fires on release, before refresh
13  onRelease: () => analytics.track('pull'),
14
15  // optional: pixels to drag (default: 100)
16  threshold: 80,
17});
18
19// ③ Clean up (e.g. SPA route change)
20pully.destroy();

Ship in 5 minutes.

One command. One constructor. One callback.

$ npm install pullyjs

Or drop the script tag — no build step needed:

<script src="pully.js"></script>

View on GitHub → See the test page