Skip to content

ExpressiveCSS

Browse documentation

Pull-to-refresh

Refreshes a list or page when a person pulls down at its top, or presses its refresh button.

View as Markdown

Use .pull-to-refresh to let people fetch the latest items by pulling down at the top of a list on a touch screen. A refresh button inside the container does the same for everyone else.

Put a loading indicator in the container as a direct child, then a <button data-refresh>, then the content. AutoInit() starts every .pull-to-refresh. A touch pull from the top brings the indicator down with the finger. Releasing past the threshold starts a refresh, and the indicator stays until the page says the refresh is done. On a touch screen, pull the list below down; elsewhere, press Refresh.

  • Build passed on main
  • Invoice 2041 is ready
  • Design review moved to Thursday
  • New comment on the roadmap
  • Your export finished
  • Weekly usage report
  • Reminder: update your password
  • Two new sign-ups today
  • Release notes draft
  • Lunch on Friday?
<div class="pull-to-refresh" style="height: 24rem; overflow-y: auto">
  <span class="loading-indicator contained" role="status" aria-label="Refreshing"></span>
  <button type="button" class="text" data-refresh>Refresh</button>
  <ul class="list">
    <li>Inbox</li>
  </ul>
</div>

Finishing a refresh

A refresh fires expressive:refresh from the container and runs the onRefresh option. Pass the work's promise to event.detail.waitUntil(). The indicator stays, and the container has aria-busy="true", until every promise passed in settles, whether it succeeds or fails. Or call complete() when the work is done. Until one of those happens, the refresh keeps running, and further pulls and presses do nothing. A rejection the page does not catch still reaches unhandledrejection, and an onRefresh that throws ends the refresh it started.

The script adds a visually hidden status region to the container. It announces "Refreshing" when a refresh starts and "Refreshed" when it finishes, so screen reader users hear both. Set the words with the i18n option.

document.addEventListener('expressive:refresh', (event) => {
  event.detail.waitUntil(loadLatest());
});

The refresh button

Pulling needs a touch screen, so every container needs its <button data-refresh>. Keyboard, mouse and switch users refresh with it, and a semantics rule flags a container without one. A mouse drag never pulls. The refresh() method starts the same refresh from script.

What scrolls

The container can scroll itself, with overflow-y: auto or scroll, or sit in a page that scrolls. Either way, a pull counts only at a scroll position of 0, and a drag that moves up or sideways first scrolls as usual. So does a drag that a scrolled-down scroller inside the container, or around it, can take. A second finger ends a pull, so a pinch still zooms. The container has overscroll-behavior-y: contain, and so does the page while it holds one, so the browser's own pull-to-refresh does not also run. Don't nest one .pull-to-refresh inside another.

Under reduced motion the indicator hides at once instead of sliding back up.

Options

Name Type Default Description
threshold Number 64 How far a finger must pull down, in pixels, for the release to refresh. A value that is not a positive number falls back to 64.
onRefresh Function null Called when a refresh starts, with the event's detail.
i18n Object { refreshing: 'Refreshing', refreshed: 'Refreshed' } What the status region announces when a refresh starts and when it finishes.

Methods

const refresher = Expressive.PullToRefresh.getInstance(document.querySelector('.pull-to-refresh'));
refresher.refresh();  // starts a refresh; does nothing while one runs
refresher.complete(); // ends it: the indicator goes back up, aria-busy is removed
refresher.destroy();  // removes listeners and the status region, ends a refresh without an event

Tokens

Override these on the .pull-to-refresh.

Token Default
--md-comp-pull-to-refresh-indicator-offset16px
--md-comp-pull-to-refresh-motiondefault spatial spring
--md-comp-pull-to-refresh-motion-durationthe default spatial spring's duration

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow