PullToRefresh
MD3 pull-to-refresh (Compose PullToRefreshBox) — the interaction the
loading indicator is designed for. Pull the
list down from its top (touch, mouse or pen): the indicator follows at half
the finger speed, shows the pulled fraction as a determinate indicator, and
releasing past the 80px threshold calls onRefresh. While refreshing it
sits at the threshold with an indeterminate indicator.
const [refreshing, setRefreshing] = useState(false)
const reload = async () => {
setRefreshing(true)
await fetchMessages()
setRefreshing(false)
}
<Button variant="text" onClick={reload}>Refresh</Button>
<PullToRefresh refreshing={refreshing} onRefresh={reload} style={{ height: 480 }}>
<List>{messages}</List>
</PullToRefresh>
The scroll container
PullToRefresh is the scroll container (overflow-y: auto): give it a
height — or a flex / grid track — and put the content inside. ref,
onScroll, className and style land on that element. A pull starts only
while it, and every scrollable element between it and the pointer, is
scrolled to the top, so nested scrollers do not trigger it by accident. The
page (window) scroll is not covered.
Indicators
indicator="loading"(default) — the Expressive contained loading indicator: 48dp, PrimaryContainer / OnPrimaryContainer, no elevation. It morphs from a circle to the soft-burst shape while pulling and keeps turning past the threshold.indicator="circular"— the classic indicator: a 40dp SurfaceContainerHigh disc at elevation level 2 with an arrow arc that becomes a spinner.
Refreshing state
refreshing is controlled. Leave it out and return a promise from
onRefresh to let the component show the refresh until the promise settles:
<PullToRefresh onRefresh={() => queryClient.invalidateQueries()}>…</PullToRefresh>
disabled turns the gesture off (a refresh in progress is still shown), and
threshold changes the release distance (px of indicator travel, after the
0.5 drag resistance).
Accessibility
- The pull gesture is not keyboard-reachable. Always provide a visible refresh control as well — a button in the app bar, a menu item, or a keyboard shortcut — that runs the same refresh.
- While refreshing, the container has
aria-busy="true"and a polite live region (role="status", outside the busy container) announcesrefreshingLabel(default "Refreshing"; localize it). - The indicator is decorative to assistive technology (
aria-hidden); the live region carries the announcement. - Under
prefers-reduced-motion: reducethe indicator does not move with the finger: it fades in at its resting position.
Styling hooks
The root exposes data-indicator, data-refreshing, data-pulling,
data-disabled, and --md-pull-to-refresh-progress (the indicator's
distance fraction: 0 at rest, 1 at the threshold, up to 2 when over-pulled).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables the pull gesture (an ongoing refresh is still shown). |
indicator | "loading" | "circular" | 'loading' | Indicator flavor. |
onRefresh | (() => void | Promise<unknown>) | — | Fires when a pull is released past threshold. Return a promise to keep
the indicator spinning until it settles when refreshing is not
controlled. |
refreshing | boolean | — | Controlled refreshing state (Compose isRefreshing): the indicator sits
at the threshold and spins while true. Omit it to let the component
track the promise returned by onRefresh instead. |
refreshingLabel | string | 'Refreshing' | Text announced by the polite live region when refreshing starts. |
threshold | number | 80 | Distance (px, after the 0.5 drag resistance) a pull must exceed to
refresh on release (Compose PositionalThreshold 80dp). |