Skip to content

MazPullToRefresh ​

MazPullToRefresh adds a native-feeling pull to refresh gesture to your app or to any scroll container, with a progress ring, haptic feedback and translated, accessible feedback.

Before use it, you should install the MazUi plugin in your project, follow instructions in Getting Started

Key Features ​

  • Works on the window and on any scroll container: the closest scrollable ancestor is detected automatically, so it fits app shells with a fixed layout and a scrolling <main>
  • Starts only when the container is scrolled to the top, never in the middle of a list
  • Rubber band resistance beyond the trigger distance, and a progress ring that fills while pulling
  • Ignores horizontal swipes and multi-touch gestures, and prevents the native bounce (and the browser's own pull to refresh) while pulling
  • Haptic feedback when the trigger distance is reached (devices supporting navigator.vibrate)
  • Texts from @maz-ui/translations, overridable with the translations prop or the slots
  • Accessible: the refresh is announced to screen readers, aria-busy is set while refreshing, and refresh() is exposed for a button usable with a keyboard or a mouse
  • Optional standalone-mode to enable it only in an installed PWA (browsers already have their own pull to refresh)
  • Built on the headless usePullToRefresh composable

Demo ​

Pull down inside the box on a touch device (or with the touch emulation of your browser's device toolbar).

  • Item 1
  • Item 2
  • Item 3
  • Item 4
  • Item 5
  • Item 6
  • Item 7
  • Item 8
  • Item 9
  • Item 10
  • Item 11
  • Item 12
  • Item 13
  • Item 14
  • Item 15
  • Item 16
  • Item 17
  • Item 18
  • Item 19
  • Item 20

Basic usage ​

Wrap your app (or a page) inside the component and give it the function that reloads your data. The spinner stays visible until the returned promise settles.

vue
<script lang="ts" setup>
import MazPullToRefresh from 'maz-ui/components/MazPullToRefresh'

async function refresh() {
  await fetchData()
}
</script>

<template>
  <MazPullToRefresh :on-refresh="refresh">
    <main>App content</main>
  </MazPullToRefresh>
</template>

With Nuxt, refreshNuxtData reloads every useFetch / useAsyncData of the page:

vue
<template>
  <MazPullToRefresh :on-refresh="() => refreshNuxtData()" standalone-mode>
    <NuxtPage />
  </MazPullToRefresh>
</template>

Inside a scroll container ​

The pull can only start when its scroll container is at the top. By default, the component uses its closest scrollable ancestor (overflow-y: auto | scroll), or the window if there is none. Typical app shells with a fixed layout and a scrolling <main> work without configuration:

vue
<template>
  <div class="app-shell fixed inset-0 flex flex-col">
    <header>…</header>
    <main class="flex-1 overflow-y-auto">
      <MazPullToRefresh :on-refresh="refresh">
        <slot />
      </MazPullToRefresh>
    </main>
  </div>
</template>

Use container-selector when the scroll container is not an ancestor of the component:

vue
<template>
  <MazPullToRefresh container-selector="#page-scroller" :on-refresh="refresh">
    …
  </MazPullToRefresh>
</template>

Installed apps only ​

Browsers already provide their own pull to refresh. With standalone-mode, the component is only active when the app runs as an installed PWA (display-mode: standalone):

vue
<template>
  <MazPullToRefresh standalone-mode :on-refresh="refresh">
    …
  </MazPullToRefresh>
</template>

Tuning the gesture ​

PropDefaultEffect
distance80Pull distance (px, after resistance) needed to trigger a refresh
max-distancedistance * 1.6Upper bound of the rubber band beyond distance
resistance0.5Ratio between the finger movement and the pull distance (lower is stiffer)
min-duration400Minimum duration (ms) of the refreshing state, so fast refreshes still show the feedback
haptictrueShort vibration when distance is reached

Customize the indicator ​

The three states have their own slot. Each one receives the pull progress (0 to 1) and the current distance in px.

vue
<template>
  <MazPullToRefresh :on-refresh="refresh" header-class="bg-surface-600">
    <template #pull-before="{ progress }">
      <span>{{ Math.round(progress * 100) }}%</span>
    </template>
    <template #pull-ready>
      <span>Let go!</span>
    </template>
    <template #pull-loading>
      <MazSpinner color="secondary" />
    </template>

    <main>App content</main>
  </MazPullToRefresh>
</template>

spinner-color sets the color of the progress ring, the ready arrow and the default spinner (primary by default).

Translations ​

Texts come from @maz-ui/translations (pullToRefresh.pull, pullToRefresh.release, pullToRefresh.refreshing). Override them per instance with the translations prop:

vue
<template>
  <MazPullToRefresh
    :on-refresh="refresh"
    :translations="{ pull: 'Swipe down', release: 'Release', refreshing: 'Updating…' }"
  >
    …
  </MazPullToRefresh>
</template>

Accessibility ​

The gesture is touch only. To give keyboard and mouse users the same action, call the exposed refresh() from a button: the indicator and the events behave exactly as with the gesture.

vue
<script lang="ts" setup>
import { useTemplateRef } from 'vue'

const pullToRefresh = useTemplateRef('pullToRefresh')
</script>

<template>
  <MazBtn :loading="pullToRefresh?.isRefreshing" @click="pullToRefresh?.refresh()">
    Refresh
  </MazBtn>

  <MazPullToRefresh ref="pullToRefresh" :on-refresh="refresh">
    …
  </MazPullToRefresh>
</template>

While refreshing, the root element has aria-busy="true" and the refreshing text is announced through a polite live region. The visual indicator is hidden from assistive technologies.

Events ​

The events follow the life of a refresh: start, then loaded and response (with the value resolved by on-refresh) or error, and always finish. An error thrown by on-refresh is emitted, never thrown again.

Headless usage ​

Need a different indicator, or a gesture on an element without wrapping it? Use the usePullToRefresh composable.

Props ​

NameDescriptionTypeRequiredDefaultPossible values
on-refreshFunction called when the user releases after pulling far enough. The spinner stays visible until the returned promise settles. Without it, the component does nothing.(() => unknown)No--
distancePull distance (in px, after resistance) required to trigger a refreshnumberNo80-
max-distanceMaximum pull distance (in px), reached with a rubber band effect beyond distancenumberNodistance * 1.6-
resistanceRatio between the finger movement and the pull distance, between 0 and 1. Lower is stiffer.numberNo0.5-
min-durationMinimum duration (in ms) of the refreshing state, so the feedback stays visible on fast refreshesnumberNo400-
container-selectorCSS selector of the scroll container. By default, the closest scrollable ancestor of the component, or the window. The pull only starts when this container is scrolled to the top.stringNo--
header-classClass of the header that holds the indicatorClassValueNo--
spinner-colorColor of the progress ring and the spinner"primary" | "secondary" | "accent" | "info" | "success" | "warning" | "destructive" | "contrast" | "transparent"No"primary"'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'destructive' | 'contrast' | 'transparent'
disabledDisable the pull to refreshbooleanNofalse-
standalone-modeOnly enable the pull to refresh when the app runs as an installed PWA (standalone display mode), browsers already provide their own pull to refreshbooleanNofalse-
hapticVibrate briefly when the pull reaches distance (devices supporting navigator.vibrate)booleanNotrue-
translationsTranslations of the pull to refresh{ pull?: string | undefined; release?: string | undefined; refreshing?: string | undefined; }NoTranslations from-
offsetnumberNo--

Events ​

Event namePayloadDescription
loaded-Emitted when onRefresh resolved
errorerror: unknownEmitted when onRefresh threw or rejected
start-Emitted when the refresh starts
responseresponse: unknownEmitted with the value resolved by onRefresh
finish-Emitted when the refresh ends, after loaded or error

Slots ​

NameDescriptionBindings
pull-beforeContent shown while pulling, before the thresholdprogress number
distance number
pull-readyContent shown once releasing triggers a refreshprogress number
distance number
pull-loadingContent shown while refreshingprogress number
distance number
defaultContent of the page-

Expose ​

refresh ​

Run the refresh programmatically (a "refresh" button for keyboard and mouse users)

isRefreshing ​

onRefresh is running

progress ​

Pull progress towards distance, from 0 to 1