Skip to main content

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.

Loading indicator
  • Message 1Fetched in round 1
  • Message 2Fetched in round 1
  • Message 3Fetched in round 1
  • Message 4Fetched in round 1
  • Message 5Fetched in round 1
  • Message 6Fetched in round 1
  • Message 7Fetched in round 1
  • Message 8Fetched in round 1
  • Message 9Fetched in round 1
  • Message 10Fetched in round 1
Circular indicator
  • Message 1Fetched in round 1
  • Message 2Fetched in round 1
  • Message 3Fetched in round 1
  • Message 4Fetched in round 1
  • Message 5Fetched in round 1
  • Message 6Fetched in round 1
  • Message 7Fetched in round 1
  • Message 8Fetched in round 1
  • Message 9Fetched in round 1
  • Message 10Fetched in round 1
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) announces refreshingLabel (default "Refreshing"; localize it).
  • The indicator is decorative to assistive technology (aria-hidden); the live region carries the announcement.
  • Under prefers-reduced-motion: reduce the 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​

PropTypeDefaultDescription
disabledbooleanfalseDisables 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.
refreshingboolean—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.
refreshingLabelstring'Refreshing'Text announced by the polite live region when refreshing starts.
thresholdnumber80Distance (px, after the 0.5 drag resistance) a pull must exceed to refresh on release (Compose PositionalThreshold 80dp).