tetra-ui Logotetra ui

Swipeable

Swipe leading and trailing actions onto row content.

swipeable

Installation

npx shadcn@latest add @tetra-ui/swipeable

Setup

Wrap your app in GestureHandlerRootView (or use the tetra-ui ThemeProvider, which already includes it):

import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function RootLayout({ children }) {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>{children}</GestureHandlerRootView>
  );
}

Usage

import {
  Swipeable,
  SwipeableAction,
  SwipeableActionGroup,
  SwipeableContent,
  SwipeableList,
} from "@/components/ui/swipeable";
<SwipeableList>
  <Swipeable>
    <SwipeableContent onPress={() => {}}>
      <Text>Message from Expo</Text>
    </SwipeableContent>

    <SwipeableActionGroup edge="leading" allowsFullSwipe={false}>
      <SwipeableAction onPress={() => {}}>Pin</SwipeableAction>
    </SwipeableActionGroup>

    <SwipeableActionGroup edge="trailing">
      <SwipeableAction variant="destructive" onPress={() => {}}>
        Delete
      </SwipeableAction>
    </SwipeableActionGroup>
  </Swipeable>
</SwipeableList>

Press vs swipe

Row presses must participate in the gesture-handler system so a swipe does not also fire onPress. Put onPress on SwipeableContent (gesture-handler Pressable). Do not nest React Native’s Pressable — including InlineListItem’s onPress — inside a swipeable row; keep InlineListItem presentational and press via SwipeableContent.

Exclusive open

Wrap sibling rows in SwipeableList so opening one closes the others (Mail-style). Rows register through context, so nesting inside InlineList or a map still works. Use useSwipeableList().closeAll() to shut every row (for example when the list scrolls or navigates away). Pass exclusive={false} to keep closeAll without auto-closing siblings.

Composition

SwipeableList
└── Swipeable
    ├── SwipeableContent
    ├── SwipeableActionGroup
    │   └── SwipeableAction
    │       ├── SwipeableActionIcon
    │       └── SwipeableActionText
    └── SwipeableActionGroup

With Inline List

Wrap each row in Swipeable, and wrap the list in SwipeableList for exclusive open. Separators still apply automatically — InlineList provides separator context through any wrapper to nested InlineListItems:

import {
  InlineList,
  InlineListItem,
  InlineListItemTitle,
} from "@/components/ui/inline-list";
import {
  Swipeable,
  SwipeableAction,
  SwipeableActionGroup,
  SwipeableContent,
  SwipeableList,
} from "@/components/ui/swipeable";

<SwipeableList>
  <InlineList title="Inbox">
    <Swipeable>
      <SwipeableContent onPress={() => {}}>
        <InlineListItem>
          <InlineListItemTitle>Message from Expo</InlineListItemTitle>
        </InlineListItem>
      </SwipeableContent>
      <SwipeableActionGroup edge="trailing">
        <SwipeableAction variant="destructive" onPress={() => {}}>
          Delete
        </SwipeableAction>
      </SwipeableActionGroup>
    </Swipeable>
    <Swipeable>
      <SwipeableContent>
        <InlineListItem>
          <InlineListItemTitle>Welcome to tetra-ui</InlineListItemTitle>
        </InlineListItem>
      </SwipeableContent>
      <SwipeableActionGroup edge="trailing">
        <SwipeableAction variant="secondary" onPress={() => {}}>
          Archive
        </SwipeableAction>
        <SwipeableAction variant="destructive" onPress={() => {}}>
          Delete
        </SwipeableAction>
      </SwipeableActionGroup>
    </Swipeable>
  </InlineList>
</SwipeableList>

Full swipe

Set allowsFullSwipe on SwipeableActionGroup (defaults to true):

  • Overshoot bleeds the outermost action’s color (trailing → last child, leading → first child)
  • Dragging past about halfway across the screen arms that action (sibling labels/icons fade)
  • Pull back before release to disarm — the action only runs on gesture release while still armed
  • Works for both leading and trailing groups (allowsFullSwipe defaults to true)
  • Set allowsFullSwipe={false} to keep rubber-band overshoot without arming or auto-performing

Icons

Use SwipeableActionIcon for icon-only or icon + label actions. Labels stack under the icon and stay centered. Add an accessibilityLabel when there is no visible text:

import { ArchiveIcon, TrashIcon } from "@/components/ui/icons";
import {
  Swipeable,
  SwipeableAction,
  SwipeableActionGroup,
  SwipeableActionIcon,
  SwipeableContent,
} from "@/components/ui/swipeable";

{/* Icon only */}
<SwipeableAction accessibilityLabel="Delete" variant="destructive" onPress={() => {}}>
  <SwipeableActionIcon>
    <TrashIcon />
  </SwipeableActionIcon>
</SwipeableAction>

{/* Icon + text */}
<SwipeableAction variant="secondary" onPress={() => {}}>
  <SwipeableActionIcon>
    <ArchiveIcon />
  </SwipeableActionIcon>
  Archive
</SwipeableAction>

API

SwipeableList

Wraps sibling Swipeable rows so opening one can close the others. Registration is context-based, so rows may be nested in InlineList, fragments, or mapped children.

Prop

Type

useSwipeableList() returns { closeAll } for the nearest list (no-op outside one).

Swipeable

Built on ReanimatedSwipeable. Forwards friction, thresholds, open/close callbacks, enabled, and a ref with close / openLeft / openRight / reset.

Prop

Type

SwipeableContent

Required slot for the row content revealed by default. The content surface uses bg-card so actions slide out from behind an opaque row. When onPress is set, the surface uses gesture-handler Pressable so taps do not fire during a swipe.

Prop

Type

SwipeableActionGroup

Action group revealed when swiping from an edge.

Prop

Type

SwipeableAction

Pressable action button. String children become SwipeableActionText.

Prop

Type

SwipeableActionText

Label text styled from the parent SwipeableAction context.

SwipeableActionIcon

Clones a single child icon with size and color from the parent SwipeableAction context.

Prop

Type

On this page