Skip to content
System

Pull to Refresh

The gesture every phone app has: drag the list down to refetch, against a rubber band, with a morphing MD3 shape for an indicator.

Walkthrough

1. Basic

Drag the list down and let go. Past the threshold the shape arms; releasing runs onrefresh and holds the indicator until the promise settles.

Zainab Hassan Out for delivery 2m
Omar Ali Out for delivery 9m
Dilan Rashid Out for delivery 14m
Sara Mahmoud Out for delivery 21m
Yusuf Karim Out for delivery 33m
Lana Abdullah Out for delivery 48m
<script lang="ts">	import { Badge, List, ListItem, PullToRefresh } from 'omaris';​	let rows = $state([		{ name: 'Zainab Hassan', ago: 2 },		{ name: 'Omar Ali', ago: 9 },		{ name: 'Dilan Rashid', ago: 14 },		{ name: 'Sara Mahmoud', ago: 21 },		{ name: 'Yusuf Karim', ago: 33 },		{ name: 'Lana Abdullah', ago: 48 }	]);​	async function reload() {		await new Promise((done) => setTimeout(done, 1200));		rows = [{ name: 'New order', ago: 0 }, ...rows].slice(0, 6);	}</script>​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<PullToRefresh onrefresh={reload} height="16rem">		<List variant="plain" dividers>			{#each rows as row, i (row.name + i)}				<ListItem headline={row.name} supportingText="Out for delivery">					{#snippet trailing()}<Badge>{row.ago}m</Badge>{/snippet}				</ListItem>			{/each}		</List>	</PullToRefresh></div>

2. Overlay

overlay leaves the content where it is and floats the indicator over it — the Android behaviour, and the right one when the first row is a header you would rather not push off the top.

Refreshed 0 times
Kitchen Nothing needs attention
Couriers Nothing needs attention
Payments Nothing needs attention
Stock Nothing needs attention
Reviews Nothing needs attention
<script lang="ts">	import { List, ListItem, PullToRefresh } from 'omaris';​	let count = $state(0);	const reload = () => new Promise((done) => setTimeout(() => (count++, done(null)), 900));</script>​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<PullToRefresh overlay onrefresh={reload} height="14rem">		<div class="bg-surface-container px-4 py-2 text-label-md">			Refreshed {count} time{count === 1 ? '' : 's'}		</div>		<List variant="plain" dividers>			{#each ['Kitchen', 'Couriers', 'Payments', 'Stock', 'Reviews'] as row (row)}				<ListItem headline={row} supportingText="Nothing needs attention" />			{/each}		</List>	</PullToRefresh></div>

3. Your own indicator

The indicator is a snippet, and it is handed the live gesture: distance, progress, whether a release would refresh, and whether one is running. Put anything in it — here a pill with a bar and a word, and no shapes at all.

Pull
Today 12 orders
Yesterday 12 orders
This week 12 orders
Last week 12 orders
Older 12 orders
<script lang="ts">	import { List, ListItem, PullToRefresh } from 'omaris';​	const reload = () => new Promise((done) => setTimeout(done, 1000));</script>​<div class="w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<PullToRefresh onrefresh={reload} height="14rem" threshold={64}>		{#snippet indicator({ progress, armed, refreshing })}			<div				class="mt-2 flex items-center gap-2 rounded-full bg-inverse-surface px-3 py-1.5 text-label-sm text-inverse-surface-foreground shadow-3"				style="opacity: {Math.min(1, progress * 1.5)}"			>				<span					class="h-1 w-10 max-w-full overflow-hidden rounded-full bg-inverse-surface-foreground/25"				>					<span						class="block h-full rounded-full bg-inverse-surface-foreground"						style="width: {progress * 100}%"					></span>				</span>				{refreshing ? 'Refreshing…' : armed ? 'Release' : 'Pull'}			</div>		{/snippet}​		<List variant="plain" dividers>			{#each ['Today', 'Yesterday', 'This week', 'Last week', 'Older'] as row (row)}				<ListItem headline={row} supportingText="12 orders" />			{/each}		</List>	</PullToRefresh></div>

4. Controlled

With no onrefresh the component is controlled: the gesture opens refreshing and you close it. That is the shape to use when the refetch belongs to a store, a query client or a parent that already knows about it.

Inbox
Assigned
Escalated
Closed
<script lang="ts">	import { Button, List, ListItem, PullToRefresh, Switch } from 'omaris';​	let refreshing = $state(false);</script>​<div class="flex w-full max-w-sm flex-col gap-3">	<div class="flex items-center gap-3">		<Switch bind:checked={refreshing} label="Held open" />		<Button size="xs" variant="outlined" onclick={() => (refreshing = false)}>Done</Button>	</div>​	<div class="overflow-hidden rounded-shape-lg border border-border">		<PullToRefresh bind:refreshing height="12rem">			<List variant="plain" dividers>				{#each ['Inbox', 'Assigned', 'Escalated', 'Closed'] as row (row)}					<ListItem headline={row} />				{/each}			</List>		</PullToRefresh>	</div></div>

5. Overridden

The overridden case: a different set of outlines, a disc restyled through classes, a longer pull, and scroller={false} so the page around it does the scrolling instead of a viewport of its own.

Erbil 3 couriers on shift
Baghdad 3 couriers on shift
Basra 3 couriers on shift
Duhok 3 couriers on shift
<script lang="ts">	import { List, ListItem, PullToRefresh } from 'omaris';​	const reload = () => new Promise((done) => setTimeout(done, 900));</script>​<div class="w-full max-w-sm">	<PullToRefresh		scroller={false}		onrefresh={reload}		threshold={96}		max={190}		shapes={['pill', 'gem', 'flower', 'sunny', 'boom']}		classes={{			disc: 'size-12 bg-primary-container shadow-4 ring-0',			glyph: 'bg-primary-container-foreground'		}}	>		<List variant="outlined" dividers>			{#each ['Erbil', 'Baghdad', 'Basra', 'Duhok'] as city (city)}				<ListItem headline={city} supportingText="3 couriers on shift" />			{/each}		</List>	</PullToRefresh></div>