Pull to Refresh
السحب للتحديثSwipeRefreshLayout / .refreshable
also calledrefresh control، swipe to refresh، pull down to refresh، drag to refresh، SwipeRefreshLayout، UIRefreshControl، بول تو ريفريش، السحب لأسفل للتحديث، اسحب للتحديث، التحديث بالسحب، إيماءة التحديث، سحب لإعادة التحميل
Pull to refresh is a gesture that re-fetches the data already on screen: with the list resting at its very top, the user drags it down, an indicator emerges from under the top bar, and releasing past a set distance starts the request. What separates it from every other control is that it has no resting form (no button, no label), so nobody finds it except by trying it or by watching someone else, which is why most people cannot name it. It is not infinite scroll, which fires at the end of the list and appends older items, while this fires at the start and prepends the newest. It is not a progress indicator either, because the spinning circle is the result of the gesture rather than the gesture itself, and the same circle can appear from a Refresh menu item with no drag at all. And it is not a swipe action: that gesture runs horizontally on a single row and uncovers buttons that wait to be tapped, while this one runs vertically on the whole list and uncovers nothing to press. The rule that settles it in review: the gesture begins at scroll offset zero, or it is something else. And because it is invisible, it can never be the only way to refresh a screen.
If you called it…
Live specimen
Interact with the demo. Every part is real and numbered.
Anatomy: every part, named
Hover a row to locate it in the demo above.
Build prompt
How it behaves right-to-left
This section is ours alone.
The gesture does not mirror
never mirrorsThe whole gesture lives on the block axis: the finger goes down, the list follows, and page direction touches neither. Do not multiply the delta by a direction sign and do not reach for var(--dir) the way you would on an inline-axis control, because clientY on its own is enough and transform: translateY(...) is correct in both directions. Teams that apply a blanket "every gesture flips" rule to this element land on an indicator that travels upward in the Arabic build, or on a negative threshold that never resolves.
The indicator rotation
never mirrorsThe indicator turns clockwise in Arabic exactly as it does in English, because rotation direction is a clock convention and carries no linguistic meaning to flip. A blanket [dir="rtl"] svg { transform: scaleX(-1) } turns it counter-clockwise and mirrors the pull arrow along with it, so scope the flip to a class you put on horizontal arrows by hand; transform has no logical form, so the browser will never make that call for you. In UIKit, UIRefreshControl draws its own spinner and takes no image, and if you substitute a custom one, do not call imageFlippedForRightToLeftLayoutDirection() on it: that call is an explicit request to mirror, and a vertical arrow has nothing to reverse.
Indicator text and the timestamp
mirrorsAny text that rides with the indicator ("Pull to refresh", "Last updated 9:41") follows page direction, so align it with text-align: start and not left. If you lay the circle and the text out in one row, use gap and padding-inline instead of margin-left, because the visual order of the row flips while DOM order stays put. The timestamp is its own hazard: dropping 12:04 PM or a Latin time-zone name into an Arabic sentence lets the bidi algorithm reorder the pieces on screen, so isolate the run with <bdi> or unicode-bidi: isolate and PM will stop jumping to the head of the line.
A horizontal swipe on the same list
mirrorsLayer horizontal swipe actions onto the same rows and you are on the inline axis, which mirrors completely: what a leftward drag uncovered in English is uncovered by a rightward drag in Arabic. In SwiftUI write .swipeActions(edge: .trailing), and on Android pass ItemTouchHelper.START and ItemTouchHelper.END to SimpleCallback rather than LEFT and RIGHT, because the first pair resolves against the layout direction and the second is physical. The dX value inside onChildDraw stays physical regardless (negative for leftward motion whatever the layout direction), so read the sign from layoutDirection before you decide which button to paint behind the row.
Insets around the indicator
mirrorsThe gesture is vertical, but the chrome around it is not, and the SwipeRefreshLayout circle sits centred so nothing in it mirrors on its own. The moment you offset it toward one side or park a label beside it, use padding-inline-start and margin-inline-end on the web and NSDirectionalEdgeInsets in UIKit. UIScrollView.contentInset is a UIEdgeInsets with physical left and right fields and no logical counterpart on that type, so what you set there stays where it was when the app flips and you have to swap it by hand.
In code
Each row is one framework's word for the same thing. Take the row your project speaks.
See also
Updated · 2026-08-19