Stretch refreshPro

Pull a feed down and a line stretches to tell you when to let go.

Live · keyboard ready

Brightline activity

Updated 14 min ago

  • Sarah Mitchell merged Retry failed card payments

    Checkout · 14 min ago

  • Build 2481 passed on staging

    Deploys · 22 min ago

  • Marcus Reed commented on Q4 roadmap

    “Can search move to week 3? Filters depend on it.”

    Planning · 38 min ago

  • Halden Supply paid invoice INV-2043

    $4,820.00 · 1 hr ago

  • Jenny Tran added a photo to Spring lookbook

    A living room with tan leather chairs around a low fireplace

    Brand · 2 hr ago

  • Ethan Walsh closed Safari date picker jumps

    Web app · 3 hr ago

  • Ryan Torres published September release notes

    Docs · Yesterday

  • 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.
  • 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

Pro source and install commands unlock with a Pro plan.

example.tsx
import { StretchRefresh } from "@/registry/components/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`;      }}    />  );}

Fill the height of a sidebar

example.tsx
<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>

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.

PropTypeDefaultDescription
titleRequiredstring–Heading above the feed.
subtitlestring–Short status under the heading, such as "Updated 14 min ago". A change rises in.
itemsRequiredreadonly T[]–Rows in the feed.
getKeyRequired(item: T) => string–Stable key for each row.
renderItemRequired(item: T) => ReactNode–Renders one row.
onRefreshRequired() => 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".
thresholdnumber72Pull distance in px, after resistance, that arms a refresh.
doneLabelstring"Up to date"Result shown when onRefresh resolves without one.
errorLabelstring"Couldn’t refresh"Result shown when onRefresh rejects.
refreshLabelstring"Refresh"Accessible name of the refresh button.
classNamestring–Extra class on the root.
EnterorSpace
On the refresh button, runs a refresh without pulling.
  • 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.
  • 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.
  • 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.
  • 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

Give your coding assistant the Markdown reference instead of screenshots.

  • 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.

The full library index for assistants is at /llms.txt.