Lay the action buttons underneath the row, and only ever move the row itself with `transform: translateX(dx)`. The wiring resembles swipe-to-dismiss, but the goal differs — past the threshold the row doesn't fly off screen, it just settles open by exactly the width of the buttons.
At `pointerup`, snap fully open if the drag passed half the total button width; otherwise animate back to `translateX(0)`. Opening one row should auto-close any other open row (and closing on scroll) — otherwise you end up with several rows awkwardly open at once.
Both iOS Mail and Android Gmail use this, but their defaults diverge: iOS lets a full swipe to the far edge delete instantly with no confirmation, while Android always requires an explicit tap on the revealed button. Which one to follow comes down to how costly an accidental swipe would be.
When to use
Use it for row-level actions you repeat often — archive, delete, mark as read in an inbox, chat list, or to-do list. If there is only ever one action, a long-press menu can be simpler.