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
translationsprop or the slots - Accessible: the refresh is announced to screen readers,
aria-busyis set while refreshing, andrefresh()is exposed for a button usable with a keyboard or a mouse - Optional
standalone-modeto 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.
<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:
<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:
<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:
<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):
<template>
<MazPullToRefresh standalone-mode :on-refresh="refresh">
…
</MazPullToRefresh>
</template>Tuning the gesture
| Prop | Default | Effect |
|---|---|---|
distance | 80 | Pull distance (px, after resistance) needed to trigger a refresh |
max-distance | distance * 1.6 | Upper bound of the rubber band beyond distance |
resistance | 0.5 | Ratio between the finger movement and the pull distance (lower is stiffer) |
min-duration | 400 | Minimum duration (ms) of the refreshing state, so fast refreshes still show the feedback |
haptic | true | Short 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.
<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:
<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.
<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
| Name | Description | Type | Required | Default | Possible values |
|---|---|---|---|---|---|
| on-refresh | Function 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 | - | - |
| distance | Pull distance (in px, after resistance) required to trigger a refresh | number | No | 80 | - |
| max-distance | Maximum pull distance (in px), reached with a rubber band effect beyond distance | number | No | distance * 1.6 | - |
| resistance | Ratio between the finger movement and the pull distance, between 0 and 1. Lower is stiffer. | number | No | 0.5 | - |
| min-duration | Minimum duration (in ms) of the refreshing state, so the feedback stays visible on fast refreshes | number | No | 400 | - |
| container-selector | CSS 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. | string | No | - | - |
| header-class | Class of the header that holds the indicator | ClassValue | No | - | - |
| spinner-color | Color 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' |
| disabled | Disable the pull to refresh | boolean | No | false | - |
| standalone-mode | Only enable the pull to refresh when the app runs as an installed PWA (standalone display mode), browsers already provide their own pull to refresh | boolean | No | false | - |
| haptic | Vibrate briefly when the pull reaches distance (devices supporting navigator.vibrate) | boolean | No | true | - |
| translations | Translations of the pull to refresh | { pull?: string | undefined; release?: string | undefined; refreshing?: string | undefined; } | No | Translations from | - |
| offset | number | No | - | - |
Events
| Event name | Payload | Description |
|---|---|---|
| loaded | - | Emitted when onRefresh resolved |
| error | error: unknown | Emitted when onRefresh threw or rejected |
| start | - | Emitted when the refresh starts |
| response | response: unknown | Emitted with the value resolved by onRefresh |
| finish | - | Emitted when the refresh ends, after loaded or error |
Slots
| Name | Description | Bindings |
|---|---|---|
| pull-before | Content shown while pulling, before the threshold | progress numberdistance number |
| pull-ready | Content shown once releasing triggers a refresh | progress numberdistance number |
| pull-loading | Content shown while refreshing | progress numberdistance number |
| default | Content of the page | - |
Expose
refresh
Run the refresh programmatically (a "refresh" button for keyboard and mouse users)
isRefreshing
onRefreshis running
progress
Pull progress towards
distance, from 0 to 1