# Swipe Deck

> A React swipe card deck: decide on one card at a time, swipe right to accept, left to reject, optionally up for later, with undo.

- Type: React component (Mobile)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add swipe-deck`
- Docs: https://wingo-ui.com/components/swipe-deck

## Interaction

Drag the top card with a finger or the mouse: it follows and tilts, a stamp (Accept, Reject, Later) fades in on the side you are heading for, the matching round button tints and grows, and the cards behind rise into place. Past a quarter of the card (or with a quick flick) the stamp settles at full strength and the phone ticks; let go there and the card flies off at the speed of your throw and the next one is on top, let go earlier and it springs back. With only left and right allowed, vertical swipes on the card still scroll the page and the card only gives a little up and down. The round buttons under the deck throw the card the same way; the small undo button (a circular arrow) brings the last card back, flying in from the side it left. A save that fails (a rejected onSwipe promise) brings the card back with an error pill. Keyboard: Tab to the card, Left / Right (and Up when allowed) decide with the same animation, Backspace or Cmd/Ctrl+Z undo, and focus follows to the next card and to the empty state at the end. Buttons and links inside a card stay clickable and never start a drag. Every decision is announced to screen readers. With reduced motion the card does not tilt and leaves with a short fade and slide.

## Installation

```bash
npx wingo-ui@latest add swipe-deck
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/swipe-deck` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/swipe-deck.tsx`

npm dependencies: `npm i lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { SwipeDeck, type SwipeDeckDirection } from "@/components/ui/swipe-deck"

type Receipt = { id: string; vendor: string; amount: string; category: string }

const receipts: Receipt[] = [
  { id: "exp-2041", vendor: "Whole Foods Market", amount: "$214.50", category: "Hospitality" },
  { id: "exp-2042", vendor: "FedEx", amount: "$86.00", category: "Shipping" },
  { id: "exp-2044", vendor: "Hilton Boston", amount: "$412.00", category: "Lodging" },
]

export function ApproveExpenses({ save }: { save: (id: string, approved: boolean) => Promise<void> }) {
  const [approved, setApproved] = useState(0)
  return (
    <SwipeDeck
      items={receipts}
      getItemLabel={(r) => `${r.vendor}, ${r.amount}`}
      renderCard={(r) => (
        <div className="flex h-full flex-col gap-2 p-5">
          <span className="text-[15px] font-semibold">{r.vendor}</span>
          <span className="text-4xl font-bold tabular-nums">{r.amount}</span>
          <span className="text-sm text-muted-foreground">{r.category}</span>
        </div>
      )}
      onSwipe={(r, direction: SwipeDeckDirection) => {
        if (direction === "right") setApproved((n) => n + 1)
        return save(r.id, direction === "right")
      }}
      labels={{ left: "Reject", right: "Approve", undo: "Undo", empty: `Done: ${approved} approved` }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `T[]` |  | The cards, each with a stable id. Appending while in use fills the stack; removing clamps the index. |
| `renderCard` | `(item: T, state: { index: number; isTop: boolean }) => ReactNode` |  | The card's content. The deck draws the card surface (radius, shadow, overflow) around it. |
| `getItemLabel?` | `(item: T) => string` | `"item.id"` | The card's accessible name and the text announced after a decision. |
| `directions?` | `("left" \| "right" \| "up")[]` | `"left, right"` | The allowed directions. Adding "up" gives a third choice (Later), a stamp at the bottom of the card and a smaller middle button; without "right" its button is hidden. |
| `visibleCards?` | `number` | `3` | Cards drawn in the stack, 1 to 4. The rest mount when they reach it; fewer items draw fewer cards. |
| `stack?` | `"stack" \| "fan" \| "none"` | `"stack"` | stack: the cards behind peek out below, scaled to 0.95 / 0.9. fan: they lean ±3° alternately. none: only the top card shows; the next fades in as it leaves. |
| `rotation?` | `number` | `12` | Degrees of tilt at a full-width drag, around the bottom of the card. 0 keeps it upright. |
| `leftTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"danger"` | The reject stamp and button. Stamps always carry text and an icon, so color is never the only cue. |
| `rightTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"success"` | The accept stamp and button. Brand orange suits a like on a marketplace. |
| `upTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"info"` | The later stamp and button, when "up" is allowed. |
| `radius?` | `"md" \| "lg"` | `"lg"` | Card radius: md 16px (rounded-2xl), lg 24px (rounded-3xl). |
| `showActions?` | `boolean` | `true` | The round 56px buttons under the deck: the mouse, keyboard and screen reader path to every decision. |
| `showUndo?` | `boolean` | `true` | The undo button next to them. Off also turns off Backspace and Cmd/Ctrl+Z. |
| `stamps?` | `boolean` | `true` | The labels that fade in on the card while it is dragged and lock in when a direction is armed. |
| `haptics?` | `boolean` | `true` | A medium tick when a drag arms a direction, light on commit, selection on undo, error when onSwipe rejects. Through lib/haptics, silent on desktops. |
| `disabled?` | `boolean` | `false` | Dims the deck and blocks drags, keys and buttons. |
| `index?` | `number` |  | How many cards are decided, to control the deck from outside (set 0 to start over). |
| `defaultIndex?` | `number` | `0` | The starting index while uncontrolled: how many cards count as decided on mount. At the item count the deck opens on its empty state. |
| `onIndexChange?` | `(index: number) => void` |  | Fires after every decision, undo and failed save. |
| `onSwipe?` | `(item: T, direction: SwipeDeckDirection) => (void \| Promise<unknown>)` |  | Runs once the card has left. Return a promise to save: the deck waits for it before the next decision, and a rejection brings the card back with labels.error. |
| `onUndo?` | `(item: T, direction: SwipeDeckDirection) => void` |  | Runs when a card comes back through the undo button or a key. |
| `onEmpty?` | `() => void` |  | Runs after the last card is decided (after its save resolves, when onSwipe returns a promise). |
| `emptyState?` | `ReactNode` |  | Shown in the deck's place after the last card. Defaults to <EmptyState size="sm" title={labels.empty} /> with a check; its first button gets focus. |
| `icons?` | `Partial<Record<"left" \| "right" \| "up", ReactNode>>` |  | Icons for the stamps and buttons per direction. Defaults: X, Check, Clock. |
| `setApi?` | `(api: SwipeDeckApi) => void` |  | Hands out { swipe(direction), undo(), index } to drive the deck from your own controls. swipe animates like a button press. |
| `labels?` | `Partial<SwipeDeckLabels>` |  | Every string: left "Reject", right "Accept", up "Later", undo "Undo", empty "All done", region "Cards", instructions (read with the top card; the English default names Up when it is allowed and drops Backspace without undo), decided(label, direction, remaining) for the announcement, error "Couldn't save. Try again.". English defaults in DEFAULT_SWIPE_DECK_LABELS. |
| `classNames?` | `Partial<Record<"stack" \| "card" \| "stamp" \| "actions" \| "action" \| "undo" \| "empty" \| "error", string>>` |  | Extra classes for the inner parts. The stack is aspect-[3/4]; override it here, e.g. { stack: "aspect-[4/5]" }. |
| `className?` | `string` |  | Extra classes for the root, merged last. The deck is capped at max-w-sm and centered. A thrown card flies past the deck's edges, so give a page that can scroll sideways an overflow-x-clip wrapper. |

## Parts

### DEFAULT_SWIPE_DECK_LABELS (constant)

The English strings: the button names, Undo, the empty state, the region, the instructions, the decision announcement and the save error.

## Examples

- **Three directions**: Up postpones with Later; the stamp for up sits at the bottom of the card.
- **Fan of photo cards**: Northwind Market rentals in a fanned stack, brand tone and a heart for the like.
- **Lead triage**: No stack and no tilt for a calm, business look; the card's own button is still clickable.
- **Async save that fails**: onSwipe returns a promise; a rejection brings the card back with the error.
- **Empty state with reset**: After the last card, a custom empty state whose action resets the controlled index.
- **Keyboard only**: No buttons: focus the card, then arrows decide and Backspace undoes.

## See also

- [Card](https://wingo-ui.com/components/card) ([Markdown](https://wingo-ui.com/components/card.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [List](https://wingo-ui.com/components/list) ([Markdown](https://wingo-ui.com/components/list.md))
- [Slide To Confirm](https://wingo-ui.com/components/slide-to-confirm) ([Markdown](https://wingo-ui.com/components/slide-to-confirm.md))

---

Canonical: https://wingo-ui.com/components/swipe-deck
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
