# Pull to refresh

> A scroll view whose refresh indicator is a drop that stretches as you pull, snaps round with a bounce and a haptic past the threshold, spins while onRefresh runs and ends on a check or an error.

- Type: Component (feedback)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/pull-to-refresh
- Markdown: https://goodui.dev/native/components/pull-to-refresh.md
- Registry item: https://goodui.dev/native/r/pull-to-refresh.json
- Dependencies: react-native-gesture-handler, react-native-reanimated, react-native-svg
- Keywords: react native pull to refresh, custom refresh control, expo pull to refresh, refresh indicator animation, scroll view refresh, pull down to reload

## When to use

- Screens whose content changes on the server: inboxes, feeds, order lists, account activity.
- A screen that is one scrolling column of content, where the pull starts from the top.

## When not to use

- Virtualized lists with thousands of rows (FlatList, FlashList): use their refreshControl with React Native's RefreshControl.
- The first load of a screen: show skeleton placeholders instead of an empty, pulled-open view.
- Content that updates live on its own: show the change, do not ask people to pull.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/pull-to-refresh.json
npx expo install --fix
```

The component lands in `components/goodui-native/pull-to-refresh/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs locale.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
import { PullToRefresh } from "@/components/goodui-native/pull-to-refresh/pull-to-refresh";

export function Inbox() {
  const { messages, reload } = useMessages();
  return (
    <PullToRefresh onRefresh={reload} contentContainerStyle={{ padding: 16, gap: 8 }}>
      {messages.map((m) => (
        <MessageRow key={m.id} message={m} />
      ))}
    </PullToRefresh>
  );
}
```

## Examples

### Accent indicator, custom captions

```tsx
<PullToRefresh
  tone="accent"
  onRefresh={() => api.syncOrders()}
  labels={{ success: "Orders synced", error: "Sync failed" }}
>
  {orders}
</PullToRefresh>
```

### Refreshing from outside

```tsx
<PullToRefresh refreshing={isSyncing} onRefresh={sync}>{content}</PullToRefresh>
```

## API reference

### PullToRefresh

`import { PullToRefresh } from "@/components/goodui-native/pull-to-refresh/pull-to-refresh";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` | – | Scroll content. |
| `onRefresh` (required) | `() => unknown` | – | Runs when a pull is released past the threshold. Return a promise to hold the spinner until it settles. |
| `refreshing` | `boolean` | – | Show the refreshing state from outside, for example on first load. |
| `threshold` | `number` | `72` | Pull distance in points that arms the refresh. Default 72. |
| `tone` | `"neutral" \| "accent"` | `"neutral"` | Indicator color: `neutral` foreground or the theme `accent`. |
| `haptic` | `HapticName \| false` | `"tap"` | Haptic when the pull passes the threshold. `false` turns it off. |
| `labels` | `Partial<PullToRefreshLabels>` | – | Overrides for the built-in strings (captions and the refresh strip); a GooduiLocaleProvider pack sets them app-wide. |
| `webHandle` | `boolean` | `true` | On the web, a strip at the top that can be dragged with a mouse or clicked to refresh. Screen reader users get it as a Refresh button on every platform. Default `true`. |
| `scrollY` | `SharedValue<number>` | – | Receives the vertical scroll offset, for example to collapse a LargeTitleHeader above the list. It is measured from the top of your content, below the refresh strip when that shows, so a LargeTitle collapses on time. |
| `contentContainerStyle` | `StyleProp<ViewStyle>` | – | Style of the scroll content container. |
| `style` | `StyleProp<ViewStyle>` | – | Style for the outer container. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## Accessibility

- With a screen reader on, a Refresh button appears at the top of the content on every platform, so nobody has to perform the pull gesture.
- The scroll view reports busy while refreshing; the result is announced with the success or error label.
- The indicator itself is hidden from screen readers.
- On the web the refresh strip is named by its visible text and Enter or Space refreshes.

## Motion

- The content follows the finger with growing resistance; the drop in the gap stretches with the pull.
- Passing the threshold snaps the drop round on the bouncy spring and turns its arrow upward; pulling back un-arms it on the snappy spring.
- Releasing armed holds the content open on the smooth spring while a spinner runs; the result shows a drawn check or an alert for 750 ms, then everything tucks away.
- Reduced motion: the drop never stretches or bounces, the arrow flips, and settling uses a short timing curve instead of a spring.

## Haptics

- tap when the pull passes the threshold (configurable with the haptic prop)
- success when onRefresh resolves, error when it rejects

## Performance

- The pull, the drop and the content offset run on the UI thread; React renders only when the state changes (armed, refreshing, result).
- The spinner shows for at least 450 ms, so a fast refresh never flashes.

## Notes for AI

- Return the promise from onRefresh. Do not keep your own refreshing flag unless the refresh starts somewhere else; then pass it as refreshing.
- It is the screen's scroll view: do not nest it inside another ScrollView.
- The scroll view does not bounce on iOS (the pull replaces the top bounce), so content stops firmly at the bottom.
- On the web, a mouse drags the content down to refresh, and the top strip can be clicked; set webHandle={false} to hide the strip.
- To collapse a large title above the list, pass a shared value as scrollY and give the same one to LargeTitleHeader and LargeTitle (render LargeTitle as the first child). scrollY counts from the top of your content, below the web refresh strip, so the title collapses on time on every platform.

## Related

- [Skeleton](https://goodui.dev/native/components/skeleton.md): Loading placeholders whose shimmer is one light passing diagonally over the whole screen, plus presets and a Swap that eases from the skeleton's height to the content's while it cross-fades.
- [Toast](https://goodui.dev/native/components/toast.md): Background confirmations that stack at the top, fan out on tap, swipe away up or sideways, and pause their timers while touched.
- [Swipe actions](https://goodui.dev/native/components/swipe-actions.md): Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.

## Guidance for AI tools

Pull to refresh: A scroll view whose refresh indicator is a drop that stretches as you pull, snaps round with a bounce and a haptic past the threshold, spins while onRefresh runs and ends on a check or an error. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
