Dots

Swipe Row

A list row that slides aside to uncover action tiles, with a full swipe for the main action.

Swipe a row to the left

"use client"

import * as React from "react"

The SwipeRow component wraps a list row, such as a transaction or a scheduled payment, and uncovers up to three action tiles when the user swipes it to the side. A short swipe opens the tiles, and a long swipe runs the action marked isFullSwipe. The tiles are ordinary buttons in the tab order, so nothing depends on the gesture. Use it for quick, repeatable actions on rows in a long list; when an action needs a choice or a confirmation, open a menu or a sheet from the row instead.

Installation

pnpm dlx shadcn@latest add @dots/swipe-row

Usage

import {
  SwipeRow,
  SwipeRowAction,
  SwipeRowActions,
  SwipeRowContent,
} from "@/components/ui/swipe-row"
<SwipeRow>
  <SwipeRowContent>
    <TransactionRow title="Coffee Point" amount={-4.5} onPress={openDetails} />
  </SwipeRowContent>
  <SwipeRowActions aria-label="Coffee Point actions">
    <SwipeRowAction variant="destructive" isFullSwipe onAction={remove}>
      <TrashIcon />
      Delete
    </SwipeRowAction>
  </SwipeRowActions>
</SwipeRow>

Composition

Use the following composition to build a swipe row:

SwipeRow
├── SwipeRowContent
└── SwipeRowActions
    └── SwipeRowAction (up to three)

Features

  • A short swipe opens the tiles, a long swipe past 60% of the row runs the full-swipe action
  • A vertical drag scrolls the page, so the row never blocks scrolling
  • A drag, or a tap that only closes the row, never also presses the row underneath
  • Keyboard focus on a tile opens the row; Escape closes it and returns focus to the row, and moving focus away also closes it
  • Three tile variants: secondary, primary, and destructive
  • Controlled or uncontrolled open state, and right-to-left layouts

Open by default

Set defaultOpen to show the tiles on first render. Use variant="primary" for the main action of the row and variant="destructive" for delete; the rest stay secondary.

"use client"
import {
  SwipeRow,

Single action

A row can have one tile. Keyboard users reach it by tabbing past the row: the row opens when the tile gets focus, and Escape closes it and returns focus to the row.

"use client"

import * as React from "react"

Controlled

Pass open and onOpenChange to open and close the row from outside, for example from a button that teaches the gesture.

"use client"

import * as React from "react"

Disabled

Set isDisabled to turn the gesture off, for example while a transfer is still sending. The tiles stay reachable by keyboard.

"use client"
import {
  SwipeRow,

Accessibility

Each SwipeRowAction renders a React Aria Button, so it responds to mouse, touch, keyboard, and screen reader activation without the swipe. SwipeRowActions is a role="group" that opens the row when focus enters it, so the focused tile is always visible. Escape closes the row and does not also close a sheet or popover around it.

Make the row focusable

The row content must hold a focusable element, such as a TransactionRow with onPress or href. When a tile closes the row from the keyboard, or Escape closes it, focus moves to that element. Without one, focus has nowhere to go: a keyboard action or Escape leaves the row open until focus leaves it, and Escape passes through to a sheet or popover around the row.

Label the group

SwipeRowActions defaults to aria-label="Actions". Name the row it belongs to, such as "Coffee Point actions", so screen reader users know which row the tiles act on.

Tile labels

The visible text of a tile names its button. Keep it short, such as "Pin" or "Delete", and keep the icon decorative.

Moving focus after a remove

After an action the row closes and focus moves from the tile back to the focusable element in the row. An action that removes the row, such as delete or archive, must move focus itself, for example to the next row or to an undo button. The demo at the top moves focus to Undo after a delete, and back to the restored row after the last undo.

API Reference

SwipeRow

The root. Holds the open state and the gesture.

PropTypeDefaultDescription
openboolean-Whether the tiles are shown. Pair with onOpenChange.
defaultOpenbooleanfalseWhether the tiles are shown on first render when uncontrolled.
onOpenChange(open: boolean) => void-Called when the row opens or closes.
isDisabledbooleanfalseTurns the swipe gesture off. The tiles stay reachable by keyboard.
classNamestring-Additional classes to apply to the root element.
...propsOmit<React.ComponentProps<"div">, "onChange">-Props spread to the <div>.

SwipeRowContent

The row itself, such as a TransactionRow or an Item. It slides with the swipe. It must hold a focusable element, see Make the row focusable.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the content.
...propsReact.ComponentProps<"div">-Props spread to the <div>.

SwipeRowActions

The tiles behind the row, up to three. Focus entering the group opens the row.

PropTypeDefaultDescription
aria-labelstring"Actions"Names the group of tiles for assistive tech.
classNamestring-Additional classes to apply to the group.
...propsReact.ComponentProps<"div">-Props spread to the <div>.

SwipeRowAction

One tile: an icon above a short label. Renders a React Aria Button.

PropTypeDefaultDescription
variant"secondary" | "primary" | "destructive""secondary"The tile style.
onAction() => void-Runs on press, and on a full swipe when isFullSwipe is set.
isFullSwipebooleanfalseA long swipe stretches this tile across the row and runs onAction. One per row.
closeOnActionbooleantrueCloses the row after the action runs.
classNamestring-Additional classes to apply to the tile.
...propsOmit<ButtonProps, "className" | "onPress">-React Aria Button props, such as isDisabled or aria-label.