flutter_swipe_actions
Smooth swipe actions for Flutter lists, inspired by the iOS Mail interaction.
The package includes:
SwipeActions: wraps a list item and reveals leading or trailing actions.SwipeController: keeps only one row open at a time and exposes open/close helpers.SwipeActionButton: a reusable circular action button with a label.- RTL-aware direction handling.
- Animated item decoration and action transitions.
- Progressive reveal: actions appear only when the current swipe distance has enough room for them.
- Optional auto-close when an action is tapped.
Usage
final swipeController = SwipeController();
SwipeActions(
controller: swipeController,
leadingActions: [
SwipeActionButton(
icon: const Icon(Icons.mark_email_unread_outlined),
label: 'Unread',
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
onPressed: () {},
),
],
trailingActions: [
SwipeActionButton(
icon: const Icon(Icons.flag_outlined),
label: 'Flag',
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
onPressed: () {},
),
SwipeActionButton(
icon: const Icon(Icons.archive_outlined),
label: 'Archive',
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
onPressed: () {},
),
],
child: const ListTile(
title: Text('Weekly update'),
subtitle: Text('Your message is ready to review.'),
),
)
Controller
Use a shared SwipeController for every row in the same list:
final swipeController = SwipeController();
// Programmatically close the currently open item.
swipeController.closeAll();
// Read stable state without depending on an internal State object.
final hasOpenItem = swipeController.hasOpenItem;
final openSide = swipeController.openSide;
This prevents multiple rows from staying open at the same time.
Action behavior
SwipeActionButton closes the nearest SwipeActions row after it is tapped.
This matches the usual Mail-style interaction:
SwipeActionButton(
icon: const Icon(Icons.archive_outlined),
label: 'Archive',
onPressed: archiveMessage,
)
For multi-step actions, loading states, or confirmation flows, keep the row open:
SwipeActionButton(
icon: const Icon(Icons.more_horiz),
label: 'More',
closeOnPressed: false,
onPressed: showMoreActions,
)
Custom action widgets can close the nearest row with:
SwipeActions.maybeOf(context)?.closeActions();
Callbacks
onOpened and onClosed fire after the animation reaches its final state.
Use onOpenChanged when you need the side that opened:
SwipeActions(
onOpenChanged: (side) {
if (side == null) {
debugPrint('Closed');
} else {
debugPrint('Opened $side');
}
},
trailingActions: actions,
child: child,
)
Visual tuning
SwipeActions accepts any Widget as an action, but SwipeActionButton is the
recommended default for normal list actions. Keep custom widgets for special
cases such as loading states, badges, or multi-step actions.
By default, action widths are adaptive. SwipeActionButton exposes its preferred
width before the first drag, so regular actions open at the right distance
immediately. Fully custom action widgets are measured after layout and use
estimatedActionWidth only as their first-frame fallback. If your design needs a
strict fixed width, pass actionWidth.
Actions are revealed progressively from the moving item edge outward. If a side has multiple actions, the first declared action is the first one shown and stays closest to the item, the next action appears when the revealed swipe space can fit both, and so on. While closing, actions disappear in the same width-aware order. Each action scales in and out, keeping the reveal smooth without allowing hidden actions to receive taps or semantics focus.
The default closed surface uses Theme.of(context).colorScheme.surface, so the
item does not visually change before the swipe starts. You can customize both
closed and opened colors without rebuilding full decorations:
SwipeActions(
// Optional. Remove this line to keep adaptive action widths.
actionWidth: 72,
actionSpacing: 4,
closedBackgroundColor: Theme.of(context).colorScheme.surface,
openedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
itemBorderRadius: BorderRadius.circular(20),
trailingActions: const [],
child: child,
)
When actionWidth is null, these values control the adaptive width behavior:
SwipeActions(
// First-frame fallback for custom action widgets.
estimatedActionWidth: 96,
minActionWidth: 56,
maxActionWidth: 96,
actionSpacing: 6, // also used between item and first action
trailingActions: actions,
child: child,
)
Compatibility
actions is kept as an alias for trailing actions:
SwipeActions(
actions: [SwipeActionButton(...)],
child: child,
)
For new code, prefer leadingActions and trailingActions.