Swipe Actions
إجراءات السحب.swipeActions / ItemTouchHelper
also calledswipe buttons، swipe to reveal، row actions، swipe to delete، leading and trailing actions، swipeable list row، سوايب أكشنز، أزرار السحب، إجراءات سحب الصفّ، الأزرار المخبّأة خلف الصفّ، السحب للحذف، إجراءات جانبية للصفّ
Swipe actions are buttons parked behind a row in a list. They come into view when the user drags the row sideways, and the row keeps its place in the list the whole time. Each of the two row edges carries its own set: the leading edge usually holds the light, reversible action, and the trailing edge holds the heavier one such as delete. It is not pull to refresh, which travels on the vertical axis and belongs to the whole list rather than to a single row. A long-press context menu is a different thing again: once it opens it is on screen, with a position and readable items, while nothing about a swipe action is visible before the drag begins. Swipe to dismiss is not this either, because there one outcome is wired straight to the gesture: cross the threshold and the row leaves the list, with no button revealed at any point. The practical consequence is that swipe actions stay invisible until somebody stumbles onto them, so everything you put behind a row needs a second route the user can actually see.
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.
Leading and trailing swap
mirrorsThe two edges are logical, so they trade places with interface direction: what you defined as leading shows up on the right of the screen in Arabic, and what you defined as trailing shows up on the left. That is the platform behaving correctly, because SwiftUI takes a HorizontalEdge in .swipeActions(edge:), UIKit splits tableView(_:leadingSwipeActionsConfigurationForRowAt:) from tableView(_:trailingSwipeActionsConfigurationForRowAt:), and both resolve against the effective interface layout direction. A team that wrote its spec with the words left and right will find delete under the wrong thumb after localisation: the motion the user learned as archive now means delete.
Raw deltas stay physical
never mirrorsPointer numbers know nothing about interface direction and never flip with it. Current clientX minus its value at drag start is positive for rightward motion in both directions, the dX handed to ItemTouchHelper.onChildDraw is a physical displacement too, and so is whatever UIPanGestureRecognizer.translation(in:) returns, because the view coordinate system is not mirrored. Any hand-rolled implementation therefore has to multiply the delta by a direction sign before deciding which edge was revealed: getComputedStyle(el).direction === "rtl" ? -1 : 1 on the web, LocalLayoutDirection.current == LayoutDirection.Rtl in Compose, traitCollection.layoutDirection == .rightToLeft in UIKit; ready-made components do it for you, since ItemTouchHelper defines the relative START and END and converts them to LEFT and RIGHT through convertToAbsoluteDirection according to layout direction, while LEFT and RIGHT are absolute and never move.
Where the panel anchors
mirrorsThe revealed panel anchors to the edge it belongs to, so pin the trailing panel with inset-inline-end: 0 and the leading panel with inset-inline-start: 0 rather than right and left, and both move on their own when the page flips. Inside each button, text-align: start plus padding-inline-start keeps the label and the icon on the reading side; text-align: left strands an Arabic label at the far end of a 60px button and reads as a rendering fault. The row is pushed over the panel with transform, a physical property with no logical counterpart in CSS, so write it as translateX(calc(var(--reveal) * var(--dir) * -1)) with --dir set to -1 under [dir="rtl"], or the row slides over the wrong panel and uncovers the opposite edge.
The action icons
never mirrorsAction icons do not mirror. A trash can, an archive box, a flag, and a bell are pictures of physical objects whose meaning does not change with page direction, so a blanket [dir="rtl"] svg { transform: scaleX(-1) } flips them for no reason and leaves them looking drawn back to front. Reply and forward arrows are the exception, and nothing flips them on your behalf: set android:autoMirrored="true" on the vector drawable, call imageFlippedForRightToLeftLayoutDirection() on the UIImage, and on the web scope a scaleX(-1) rule to that one icon class, since SF Symbols with an RTL variant are the only icons that handle themselves.
Clashing with the back gesture
mirrorsThe iOS back swipe starts at the leading edge and so does a leading swipe action, so interactivePopGestureRecognizer competes with it in Arabic exactly as it did in English: the pair mirrors together and only the side changes. The browser is the case that catches people out, because Safari resolves its back-swipe edge from the browser UI language and not from the dir attribute on your page, so an Arabic page inside an English Safari keeps its leading edge on the right while the back strip stays on the left. On the web, touch-action: pan-y stops the browser panning the row but cannot cancel that edge swipe, and no page-level API can, so start the drag away from both edges, and on Android 10 and later, where both edges are back zones, claim your strip with View.setSystemGestureExclusionRects.
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