Stretch refreshPro
Pull a feed down and a line stretches to tell you when to let go.
$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/stretch-refresh- Type: Component (special)
- Access: Xzero Pro
- Page: https://ui.xzero.co.uk/components/stretch-refresh
- Markdown: https://ui.xzero.co.uk/components/stretch-refresh/markdown
- Source file:
registry/components/stretch-refresh/stretch-refresh.tsx - Dependencies: motion, lucide-react
- Keywords: special, motion, react pull to refresh, pull to refresh, refresh feed, rubber band refresh, mobile pull down refresh, feed refresh animation
When to use
- Timelines, inboxes, and activity feeds where new entries arrive at the top.
- Panels that should support pull to refresh by touch, mouse drag, and trackpad scroll.
When not to use
- Use a plain list for static content that never refreshes.
- Use pagination for paging through older items.
- Use timeline for a static history of events.
Installation
Register both Xzero UI namespaces in your project's components.json:
{
"registries": {
"@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
"@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
}
}
npx shadcn@4.23.0 add @xzero-ui-pro/stretch-refresh
Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.
Usage
import { StretchRefresh } from "@/components/xzero/stretch-refresh/stretch-refresh";
export function Inbox() {
const [messages, setMessages] = useState(initial);
return (
<StretchRefresh
title="Inbox"
items={messages}
getKey={m => m.id}
renderItem={m => <MessageRow message={m} />}
onRefresh={async () => {
const fresh = await fetchNew();
setMessages(prev => [...fresh, ...prev]);
return `${fresh.length} new messages`;
}}
/>
);
}
Examples
Fill the height of a sidebar
<aside style={{ display: "flex", flexDirection: "column", height: "100dvh" }}>
<StretchRefresh
title="Activity"
items={events}
getKey={e => e.id}
renderItem={e => <EventRow event={e} />}
onRefresh={reload}
className="grow"
/>
</aside>
API reference
StretchRefresh
A feed that refreshes when pulled down past its top edge. The sheet rubber-bands while rows spread apart, a line in the gap stretches to show when to let go, then runs while the work happens.
| Prop | Type | Default | Description |
|---|---|---|---|
title (required) | string | – | Heading above the feed. |
subtitle | string | – | Short status under the heading, such as "Updated 14 min ago". A change rises in. |
items (required) | readonly T[] | – | Rows in the feed. |
getKey (required) | (item: T) => string | – | Stable key for each row. |
renderItem (required) | (item: T) => ReactNode | – | Renders one row. |
onRefresh (required) | () => Promise<string | void> | – | Runs after a pull past the threshold or a press of the refresh button. Update items before it resolves; resolve with a short result such as "3 new updates". |
threshold | number | 72 | Pull distance in px, after resistance, that arms a refresh. |
doneLabel | string | "Up to date" | Result shown when onRefresh resolves without one. |
errorLabel | string | "Couldn’t refresh" | Result shown when onRefresh rejects. |
refreshLabel | string | "Refresh" | Accessible name of the refresh button. |
className | string | – | Extra class on the root. |
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | On the refresh button, runs a refresh without pulling. |
Accessibility
- A refresh button gives keyboard and switch users the same path as the pull gesture, with aria-disabled while busy.
- The feed is a labelled, focusable region and the list sets aria-busy while refreshing.
- "Refreshing" and the result are announced in a polite status region.
Motion
- The sheet rubber-bands with growing resistance while rows spread apart; scrolling up again at the top also pulls.
- A line in the gap reaches full length exactly at the threshold, runs while refreshing, then gives way to the result as new rows settle in from the top.
- Reduced motion keeps the same states and only fades.
Responsive behavior
- The root is a flex column with min-height 0, so give it a sized parent; the feed scrolls inside it.
- Touch pulls use native listeners and only take over once the feed is at the top, so normal scrolling is untouched.
Performance
- Rows are not virtualized and use layout position animation; keep the feed to what one panel shows plus a page or so.
- Wheel and touchmove listeners are non-passive so the pull can prevent scrolling, but they return early unless pulling.
Notes for AI
- Choose it for timelines, inboxes, and activity feeds where new entries arrive at the top. For static lists use a plain list, and for paging use pagination.
- Update items inside onRefresh before it resolves, and keep the call at least half a second so the running line reads as work.
Related
- Timeline: Follow what happened, newest first, grouped by day.
- Skeleton: Reserve space while content is still loading.
- Toast: Brief confirmation for a completed background action.
- Reorderable list: Organize items with direct drag and drop.
Also in loaders
- Morph loader: Tiny loaders that morph between shapes and fold into a check or a cross when done.
- Skeleton morph: Loading skeletons that grow into the real content, block by block, instead of swapping.
- Black hole: A delete transition that pulls the chosen items into a tiny singularity while the rest of the list bends around it; undo spins them back out.
Guidance for AI tools
Stretch refresh: Pull a feed down and a line stretches to tell you when to let go. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.
Full library index: https://ui.xzero.co.uk/llms.txt