Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Mobile

Pull To Refresh

Mobile

Pull To Refresh React component for Tailwind CSS v4

Picker WheelSlide To Confirm
Live props
Appearance
variant
size
tone
color
Content
showLabel
State
refreshing
disabled
doneDuration600ms
Layout
threshold72px
maxPull128px
indicatorOffset0px
scroll
Behavior
wheel
pullWithMouse
haptics
shortcut
Code
import { PullToRefresh } from "@/components/ui/pull-to-refresh"

<PullToRefresh />
  1. Home
  2. Components
  3. Mobile
  4. Pull To Refresh
Pro

Pull To Refresh React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React pull to refresh wrapper: pull a list past its top to reload it, with rubberband resistance, iOS spokes or a Material ring, haptics.

At the top of the list, drag down with a finger: the content follows with growing resistance and the indicator appears in the gap (an arrow, the iOS spokes drawing one by one, or a Material circle whose ring fills). Past the threshold the arrow turns, the spokes complete or the ring saturates, and the phone ticks once. Let go there and it spins while onRefresh runs (500ms at least), shows a check (or an alert when the promise rejects) and the content glides back; let go earlier and it springs back. Swiping sideways or up is left to the page, and pulls never start in inputs, [data-no-pull], a sideways scroller or a nested list that is not at its own top. On a trackpad or mouse wheel, scroll up once more at the very top to pull; stop for a moment to release. With pullWithMouse a mouse drag pulls too. There is no gesture on the keyboard: bind shortcut, or call setApi().refresh() from a refresh button. Screen readers hear Refreshing, then Updated or the error.

Pull To Refresh is a Pro React component in the React mobile components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add pull-to-refresh, together with Spinner, useScrollState, useShortcut, Haptics, Motion presets and 2 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add pull-to-refresh

Pull To Refresh is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import PullToRefresh from @/components/ui/pull-to-refresh.

"use client"

import { useState } from "react"
import { RotateCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PullToRefresh, type PullToRefreshApi } from "@/components/ui/pull-to-refresh"

type Message = { id: string; from: string; text: string }

export function Inbox({ load }: { load: () => Promise<Message[]> }) {
  const [messages, setMessages] = useState<Message[]>([])
  const [api, setApi] = useState<PullToRefreshApi>()

  return (
    <div className="flex h-dvh flex-col">
      <header className="flex h-14 items-center justify-between px-4">
        <h1 className="text-lg font-semibold">Inbox</h1>
        <Button variant="ghost" size="icon" radius="full" aria-label="Reload" onClick={() => api?.refresh()}>
          <RotateCw />
        </Button>
      </header>
      <PullToRefresh
        scroll="self"
        className="min-h-0 flex-1"
        setApi={setApi}
        onRefresh={async () => setMessages(await load())}
        labels={{ pull: "Pull to check for mail", release: "Let go to check", refreshing: "Checking for mail…", done: "Up to date", error: "Could not check for mail" }}
      >
        <ul className="divide-y divide-foreground/10">
          {messages.map((m) => (
            <li key={m.id} className="px-4 py-3">
              <p className="font-medium">{m.from}</p>
              <p className="text-sm text-muted-foreground">{m.text}</p>
            </li>
          ))}
        </ul>
      </PullToRefresh>
    </div>
  )
}

Props

  • children
    ReactNode

    The list or feed that gets pulled.

  • onRefresh
    (() => void | Promise<unknown>)

    The reload. The indicator spins until the returned promise settles, and for 500ms at least so it never flashes. A rejection shows the error state.

  • variantdefault "arrow"
    "arrow" | "ios" | "material"

    arrow: an arrow that turns at the threshold and becomes a spinner; the content moves down. ios: 12 spokes that draw one by one as you pull, then spin like UIRefreshControl; the content moves down. material: a floating circle whose ring fills while you pull, then spins; the content stays put.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Indicator 20 / 24 / 28px; the material circle 36 / 40 / 44px.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The indicator color. neutral is the quiet muted gray (the material ring is black / white); the other tones use their tone text color and fill.

  • colordefault ""
    string

    Any CSS color for the indicator. Replaces the tone. Leave empty to use the tone.

  • showLabeldefault false
    boolean

    Text under the indicator that follows the state: pull, release, refreshing, done or error.

  • labels
    Partial<PullToRefreshLabels>

    The strings shown with showLabel and announced to screen readers: pull "Pull to refresh", release "Release to refresh", refreshing "Refreshing…", done "Updated", error "Couldn't refresh". Pass only the keys you translate.

  • refreshingdefault false
    boolean

    Shows the refreshing state for a reload the app started itself (the content parks at the threshold with a spinner). OR-ed with the internal state, so false never blocks a pull.

  • disableddefault false
    boolean

    No pulling (offline, editing, a filter is active). Turning it on mid-pull springs back without refreshing.

  • doneDurationdefault 600
    number

    Milliseconds the check (or the alert) stays before the content glides back. 0 skips the done state.

  • thresholddefault 72
    number

    Pixels of resisted pull that arm the refresh; also where the content rests while it refreshes.

  • maxPulldefault 128
    number

    The furthest the content moves, however far the finger goes.

  • indicatorOffsetdefault 0
    number

    Pixels from the top of the root to the indicator's resting line, for a sticky or translucent bar over the list.

  • scrolldefault "parent"
    "parent" | "self"

    parent: watches scrollTarget, else the provided scroll container (ScrollContainerProvider), else the nearest scrolling ancestor, else the window. self: the root is the scroll container (h-full overflow-y-auto overscroll-y-contain).

  • scrollTarget
    HTMLElement | Window | RefObject<HTMLElement | null> | null

    The scroller to watch when scroll is parent. null forces the window.

  • wheeldefault true
    boolean

    A trackpad or mouse wheel scrolling up at the very top pulls too, the desktop path. Momentum that carries a scroll up to the top never pulls.

  • pullWithMousedefault false
    boolean

    A mouse drag at the top pulls, for kiosks and touch emulation. Off by default so it never fights text selection and clicks.

  • hapticsdefault true
    boolean

    A "medium" tick when the pull arms and an "error" buzz when a pulled refresh fails, through lib/haptics.

  • shortcutdefault ""
    string

    A key that refreshes, in mod+k notation ("mod+r", "shift+r"). None by default.

  • setApi
    (api: PullToRefreshApi) => void

    Receives { refresh(): Promise<void> }, which runs the same flow as a pull (indicator, promise, done). Calls during a running refresh return the running promise.

  • classNames
    Partial<Record<"indicator" | "icon" | "spinner" | "label" | "content", string>>

    Extra classes for the indicator row, the arrow / check / alert icon, the spinner, the label and the content wrapper.

  • className
    string

    Extra classes for the root, merged last.

Parts

  • DEFAULT_PULL_TO_REFRESH_LABELS

    constant

    The English copy: pull "Pull to refresh", release "Release to refresh", refreshing, done "Updated" and error "Couldn't refresh".

  • PullToRefreshApi

    type

    What setApi hands you: { refresh(): Promise<void> }, to run the same refresh (indicator included) from a button or a keyboard shortcut.

Examples

Three variants

arrow turns and becomes a spinner, ios draws its spokes, material floats a ring over a list that stays put.

Custom labels

showLabel with custom labels; the header button runs the same flow through setApi().refresh().

Failure

While Offline is on the request rejects: an alert, Could not check for mail, and an error haptic.

Under a large title

scroll="parent" watches the frame's ScrollContainerProvider; the large title stays, the list moves.

Around a List

The list's own refreshing bar mirrors the promise; material keeps the rows in place.

Brand on a feed

tone="brand" material over a marketplace grid.

Disabled while filtering

Typing a search turns disabled on, so a pull on filtered results does nothing.

Dependencies

motionlucide-reactspinneruse-scroll-stateuse-shortcuthapticsmotiontonesui-config

Files

  • components/ui/pull-to-refresh.tsx

FAQ

Is the React Pull To Refresh component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Pull To Refresh?

Run npx wingo-ui@latest add pull-to-refresh. It also adds the Wingo UI items it builds on (Spinner, useScrollState, useShortcut, Haptics, Motion presets and 2 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/pull-to-refresh.

See also

Swipeable TabsListVirtual ListLarge Title HeaderuseScrollStatehookHapticslib

Used in

Mobile App Home

More React mobile components

Swipe DeckHero HeaderBottom Tab BarSticky CTA BarSlide To ConfirmAction Sheet
All 15 React mobile components

Articles using this component

  • React Native vs PWA in 2026: Which One Do You Need?

    Comparisons and alternatives · Oct 16, 2026 · 12 min read

  • Web App That Feels Native on a Phone: 12 React Fixes

    Mobile UI in React · Oct 13, 2026 · 11 min read

  • Best React Component Library for Mobile in 2026: 9 Compared

    Comparisons and alternatives · Oct 9, 2026 · 12 min read

  • Mobile-First React Components: 9 Rules With Code

    Mobile UI in React · Oct 9, 2026 · 21 min read

PreviousPicker WheelNext Slide To Confirm

Inbox4

Updated at 4:38 PM

Pull the list down; on a trackpad, scroll up past the top

threshold 72px · maxPull 128px · doneDuration 600ms · scroll parent

wheel on · pullWithMouse off · haptics on · shortcut none