Pull to Refresh
Pull-to-refresh scroll container with thresholds, resistance, async refresh state, and reduced-motion hooks.
| Package | @vizejs/ui/pull-to-refresh |
| Maturity | stable |
| Own the source | vize lib pull pull-to-refresh |
| Requires | context, id |
| Aliases | pull to refresh, swipe to refresh, refresh gesture, overscroll refresh |
| Covers | iOS UIRefreshControl, Android SwipeRefreshLayout, Ionic Refresher |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { PullToRefresh, PullToRefreshTrigger } from "@vizejs/ui/pull-to-refresh";
Or copy the source into your project with vize lib pull pull-to-refresh (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Scrollable inbox that reloads by pulling down or with the keyboard-accessible refresh button. -->
<script setup lang="ts">
import { ref } from "vue";
import { PullToRefresh, PullToRefreshTrigger } from "@vizejs/ui/pull-to-refresh";
const messages = ref(["Welcome to your inbox", "Your order has shipped", "Weekly digest"]);
const refreshCount = ref(0);
// The root is the scroll container, so it needs a bounded block size to scroll.
const scrollerSize = { blockSize: "12rem", overflow: "auto" };
async function loadNewMessages(): Promise<void> {
await Promise.resolve();
refreshCount.value += 1;
messages.value = [`New message ${refreshCount.value}`, ...messages.value];
}
</script>
<template>
<PullToRefresh
v-slot="{ state, refreshing }"
:refresh-action="loadNewMessages"
:style="scrollerSize"
>
<p role="status">
{{ refreshing ? "Refreshing…" : state === "armed" ? "Release to refresh" : "" }}
</p>
<PullToRefreshTrigger v-slot="{ refreshing: busy }">
{{ busy ? "Refreshing" : "Refresh inbox" }}
</PullToRefreshTrigger>
<ul aria-label="Messages">
<li v-for="message in messages" :key="message">{{ message }}</li>
</ul>
</PullToRefresh>
</template>
API
PullToRefresh
Source: src/families/interaction/pull-to-refresh/pull-to-refresh.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
refreshAction |
PullToRefreshHandler |
undefined |
Async refresh handler; return a promise to keep the refreshing state until it settles. |
threshold |
number |
64 |
Pull distance (after resistance) that arms a refresh on release, in CSS px. |
maxDistance |
number |
128 |
Maximum published pull distance, in CSS px. |
resistance |
number |
0.5 |
Fraction of finger travel applied to the pull distance (0–1). |
allowMouse |
boolean |
false |
Also accept mouse and pen drags (touch is always accepted). |
disabled |
boolean |
false |
Ignore gestures and triggers. |
id |
string | null |
undefined |
Id of the scroll container, referenced by PullToRefreshTrigger. |
Events
| Event | Payload | Description |
|---|---|---|
refresh |
[source: PullToRefreshSource] |
Fired when a refresh starts, with its source. |
settle |
[source: PullToRefreshSource, error: unknown] |
Fired when a refresh settles; error is the rejection reason, if any. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
PullToRefreshSlotState |
Indicator and scrollable content with the pull state. |
Exposed
| Member | Type | Description |
|---|---|---|
state |
PullToRefreshState |
Lifecycle state. |
distance |
number |
Current pull distance in CSS px after resistance (held at threshold while refreshing). |
progress |
number |
distance / threshold, clamped to 0–1. |
refreshing |
boolean |
Whether a refresh is running. |
reducedMotion |
boolean |
Whether the user prefers reduced motion (known after mount). |
root |
HTMLDivElement | null |
Rendered scroll container. |
refresh |
() => Promise<void> |
Run the refresh handler (ignored while one is running); resolves when it settles. |
PullToRefreshTrigger
Source: src/families/interaction/pull-to-refresh/pull-to-refresh-trigger.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly refreshing: boolean } |
Button contents; the keyboard and assistive-technology alternative to pulling. |
Behavior
Normative state x input -> outcome table for pull-to-refresh.vue and
pull-to-refresh-trigger.vue (@vizejs/ui/pull-to-refresh). The root is the
consumer-styled scroll container. Every row is proven by the named test in
pull-to-refresh.test.ts or pull-to-refresh-ssr.test.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| PR1 | scrolled to top | touch pull / release | downward travel × resistance (capped at maxDistance) publishes --vize-pull-distance/--vize-pull-progress, cancels native overscroll, arms at threshold; release runs refreshAction with aria-busy and the indicator held at the threshold until it settles |
pulling from the top applies resistance, arms past the threshold, and refreshes on release |
| PR2 | any | short pull / upward / cancel / scrolled | short pulls and cancels snap back, upward drags and scrolled content keep native scrolling, and none refresh | short pulls, upward drags, scrolled content, and cancels never refresh |
| PR3 | mouse / keyboard | pointer drag / trigger | mouse and pen pulls need allowMouse; PullToRefreshTrigger is the keyboard/AT alternative wired with aria-controls |
mouse drags are opt-in, and the trigger is the keyboard alternative |
| PR4 | refreshing / disabled | refresh() | concurrent refreshes coalesce, rejections settle with the error, the trigger disables while running, and disabled ignores everything |
refreshes run one at a time, report failures, and respect disabled |
| PR5 | mounted | prefers-reduced-motion | data-reduced-motion and slot reducedMotion follow the media query so consumers can drop transitions |
publishes the reduced-motion preference after mount |
| PR6 | no provider | trigger setup | throws VIZE_UI_CONTEXT_MISSING: PullToRefresh |
the trigger requires a PullToRefresh provider |
| PR7 | SSR / hydration | isolated requests | byte-identical idle markup without busy or motion data; hydration without diagnostics | renders byte-identical idle markup and hydrates without mismatches |
The subpath ships no CSS.