Swipe actions
Reveal row actions with a swipe, or from the same actions in a menu.
swipegesturelist
Preview
Swipe actions preview
Live · keyboard ready
- Ada LovelaceAnalytical engine notes
- Grace HopperCompiler design
When to use
- Touch-first triage lists such as mail, notifications, or tasks.
- Rows with one or two quick actions per side, like Archive and Mark unread.
- Lists where a full swipe should commit the main action and the row should animate away.
When not to use
- Use context-menu or dropdown-menu for desktop-first tables.
- Use reorderable-list when rows need to be dragged vertically.
- Use hold-to-confirm for a single destructive action outside a list.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "swipe-actions",
"type": "registry:ui",
"title": "Swipe actions",
"description": "Reveal row actions with a swipe, or from the same actions in a menu.",
"categories": [
"actions"
],
"dependencies": [
"@radix-ui/react-dropdown-menu",
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/swipe-actions/swipe-actions.tsx",
"type": "registry:component",
"target": "@components/manicat/swipe-actions/swipe-actions.tsx"
},
{
"path": "registry/components/swipe-actions/swipe-actions.module.css",
"type": "registry:component",
"target": "@components/manicat/swipe-actions/swipe-actions.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/swipe-actions",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/swipe-actions",
"markdown": "https://uiarc.dev/components/swipe-actions/markdown",
"tags": [
"swipe",
"gesture",
"list"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { MoreHorizontal, Share2, Star, Trash2, Undo2 } from "lucide-react";
import { SwipeActions, SwipeActionsRow } from "@/components/manicat/swipe-actions/swipe-actions";
export function SwipeActionsExample() {
return (
<SwipeActions label="Message actions">
<SwipeActionsRow
key="ada"
label="Ada Lovelace"
leading={[{ label: "Pin", icon: <Star size={16} />, onSelect: () => {}, keepRow: true }]}
trailing={[
{ label: "Archive", icon: <Star size={16} />, onSelect: () => {} },
{ label: "Delete", icon: <Trash2 size={16} />, tone: "danger", onSelect: () => {} },
]}
>
<div className="flex items-center gap-3 py-2 text-sm">
<span className="font-medium">Ada Lovelace</span>
<span className="text-(color:--text-muted)">Analytical engine notes</span>
</div>
</SwipeActionsRow>
<SwipeActionsRow
key="grace"
label="Grace Hopper"
trailing={[{ label: "Delete", icon: <Trash2 size={16} />, tone: "danger", onSelect: () => {} }]}
>
<div className="flex items-center gap-3 py-2 text-sm">
<span className="font-medium">Grace Hopper</span>
<span className="text-(color:--text-muted)">Compiler design</span>
</div>
</SwipeActionsRow>
</SwipeActions>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Inbox rows Swipe a row to reveal pin, archive and delete, like a mail app.
Inbox rows
Swipe a row to reveal pin, archive and delete, like a mail app. preview
Live · keyboard ready
- Ada LovelaceAnalytical engine notes
- Grace HopperCompiler design
inbox-rows-swipe-a-row-to-reveal-pin-archive-and-delete-like-a-mail-app-.tsxtsx
"use client";
import { Archive, MoreHorizontal, Share2, Star, Trash2, Undo2 } from "lucide-react";
import { SwipeActions, SwipeActionsRow } from "@/components/manicat/swipe-actions/swipe-actions";
const threads = [
{ name: "Ada Lovelace", subject: "Analytical engine notes" },
{ name: "Grace Hopper", subject: "Compiler design" },
];
export function SwipeActionsInboxExample() {
return (
<div className="w-full max-w-md">
<SwipeActions label="Message actions">
{threads.map((thread) => (
<SwipeActionsRow
key={thread.name}
label={thread.name}
leading={[{ label: "Pin", icon: <Star size={16} />, onSelect: () => {}, keepRow: true }]}
trailing={[
{ label: "Archive", icon: <Archive size={16} />, onSelect: () => {} },
{ label: "Delete", icon: <Trash2 size={16} />, tone: "danger", onSelect: () => {} },
]}
>
<div className="flex items-center gap-3 py-2 text-sm">
<span className="font-medium">{thread.name}</span>
<span className="text-(color:--text-muted)">{thread.subject}</span>
</div>
</SwipeActionsRow>
))}
</SwipeActions>
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | Opens a row's More actions menu from its button. |
| ArrowDown / ArrowUp | Moves between the row's actions in the menu. |
| Escape | Closes the menu or puts away an open row. |
Accessibility
- The list is a ul with role="list" and aria-label; each row is an li.
- Every row has a "More actions for <label>" menu with the same actions, so the gesture is never required.
- Revealed action buttons are aria-hidden and out of the tab order; after a removing action, focus moves to a neighbouring row.
Motion
- The row tracks the finger, rubber-bands past its last stop, and settles on a spring that carries the release velocity; a flick projects its travel.
- Past the threshold the outer action stretches across the row, then the row slides out and the list closes the gap.
- Reduced motion jumps the row to its stops and fades rows in and out instead of animating height.
Responsive behavior
- Swipes lock to the horizontal axis after the first movement, so vertical page scrolling still works on touch.
- A full swipe commits past the larger of the open width plus 48px or 56% of the row, measured with a ResizeObserver.
- Every row also has a More actions menu, so mouse and keyboard users never need the gesture.
Performance
- Drag updates a motion value directly, so rows do not re-render while following the finger.
- Rows are not virtualized and each has its own ResizeObserver; paginate long lists.
Notes for AI
- Use for short, triage-style lists on touch (mail, notifications, tasks). For desktop-first tables use context-menu or a row menu.
- Remove the item from state inside onSelect; set keepRow for actions that keep the item, such as mark as unread.
- Rows must be direct children of SwipeActions, keyed by item id, so removal and undo animate.