Skip to main content

SwipeToDismiss

MD3 swipe-to-dismiss row (Compose SwipeToDismissBox): a horizontally draggable row over a background that is revealed as it slides away. Typically wraps a ListItem inside a List.

In a list​

Swipe a row start→end to archive or end→start to delete. Each direction reveals its own background (startToEndBackground / endToStartBackground); the row slides out and onDismiss then removes it. Inside a List, SwipeToDismiss renders the <li> and the nested ListItem renders a <div>, so list semantics are kept.

  • Swipe me #1Drag left or right
  • Swipe me #2Drag left or right
  • Swipe me #3Drag left or right
  • Swipe me #4Drag left or right
<List>
{items.map((item) => (
<SwipeToDismiss
key={item.id}
onDismiss={(direction) => remove(item.id, direction)}
startToEndBackground={<ArchiveBackground />}
endToStartBackground={<DeleteBackground />}
>
<ListItem headline={item.title} />
</SwipeToDismiss>
))}
</List>

A single background is used for both directions when the direction-specific ones are omitted. enableStartToEnd / enableEndToStart turn a direction off, and threshold (default 56px) is the distance a slow release must pass to dismiss — a fling of at least 125px/s dismisses regardless of distance. Directions are logical, so they mirror in RTL.

Delete button alternative​

Swiping must not be the only way to reach an action. The controlled dismissed prop dismisses programmatically: here each row's delete button sets it, which plays the same exit animation as a swipe; onDismiss then removes the row and moves focus to the next row's button so keyboard users are not dropped to <body>.

  • Message #1Swipe left or press delete
  • Message #2Swipe left or press delete
  • Message #3Swipe left or press delete
  • Message #4Swipe left or press delete
const [dismissed, setDismissed] = useState<SwipeDismissDirection | null>(null)

<SwipeToDismiss
dismissed={dismissed}
onDismissedChange={setDismissed}
onDismiss={() => removeMessage(id)}
enableStartToEnd={false}
endToStartBackground={<DeleteBackground />}
>
<ListItem
headline="Message"
trailing={
<IconButton
icon={<Delete />}
aria-label="Delete message"
onClick={() => setDismissed('endToStart')}
/>
}
/>
</SwipeToDismiss>

Set dismissed back to null to reset the row (for example to undo, or to veto a swipe: in controlled mode, keeping the value null in onDismissedChange cancels the dismissal). Gestures are disabled while a row is dismissed.

Styling the background by direction​

The root exposes styling hooks for backgrounds that react to the swipe: data-direction (current swipe / dismiss direction), data-target (the anchor the row would settle at if released now: settled / startToEnd / endToStart), data-dismissed, data-dragging, and the --md-swipe-to-dismiss-progress custom property (0–1, swiped distance / row width).

.row[data-target='endToStart'] .deleteIcon {
transform: scale(1.2);
}
.row .background {
opacity: calc(0.4 + var(--md-swipe-to-dismiss-progress) * 0.6);
}

Accessibility​

  • Always provide a non-gesture alternative for the swipe action (a delete button, a menu item) that sets dismissed — m3 requires swipeable list items to expose their hidden actions another way.
  • Dragging starts only after an 8px touch slop, so nested buttons and clickable rows keep their clicks.
  • The background is decorative (aria-hidden); put the action's label in the alternative control.
  • onDismiss fires after the exit animation (immediately under reduced motion). If focus was inside the removed row, move it somewhere sensible, as in the delete-button demo.

Props​

PropTypeDefaultDescription
backgroundReactNode—Content revealed behind the row, for either direction (fallback).
defaultDismissedSwipeDismissDirection | nullnullInitial dismissed state when uncontrolled.
dismissedSwipeDismissDirection | null—Controlled dismissed state: `null` = settled (row in place), a direction = dismissed that way. Changing it animates the row — set a direction to dismiss programmatically (e.g. from a delete button, the non-gesture alternative m3 requires), set `null` to reset / cancel a dismissal.
enableEndToStartbooleantrueAllow end→start dismissal.
enableStartToEndbooleantrueAllow start→end (reading-direction) dismissal.
endToStartBackgroundReactNode—Content revealed while swiping end→start (overrides `background`).
onDismiss((direction: SwipeDismissDirection) => void)—Fires once the row has finished sliding out and settled in the dismissed position (immediately under reduced motion) — the place to remove the item. Also fires after a programmatic dismiss (`dismissed`).
onDismissedChange((dismissed: SwipeDismissDirection | null) => void)—Fires when a swipe is released into a dismiss anchor (at release, before the exit animation). In controlled mode, keep the value `null` to veto.
startToEndBackgroundReactNode—Content revealed while swiping start→end (overrides `background`).
thresholdnumber56Positional threshold (px) a slow release must pass, in the direction of motion, to dismiss (Compose `positionalThreshold` 56dp).