Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Mobile

Swipe Deck

Mobile

Swipe Deck React component for Tailwind CSS v4

Sticky CTA BarSwipeable Tabs
Live props
Behavior
directions
haptics
Appearance
visibleCards3
stack
rotation12°
leftTone
rightTone
upTone
radius
stamps
Layout
showActions
showUndo
State
disabled
defaultIndex0
Code
import { SwipeDeck } from "@/components/ui/swipe-deck"

<SwipeDeck />
  1. Home
  2. Components
  3. Mobile
  4. Swipe Deck
Pro

Swipe Deck React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

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.

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.

Swipe Deck is a Pro React component in the React mobile components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add swipe-deck, together with Button, Empty State, useControllableState, Haptics, Motion presets and 2 more and the npm packages lucide-react and motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add swipe-deck

Swipe Deck is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import SwipeDeck from @/components/ui/swipe-deck.

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

  • 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.

  • getItemLabeldefault "item.id"
    (item: T) => string

    The card's accessible name and the text announced after a decision.

  • directionsdefault "left, right"
    ("left" | "right" | "up")[]

    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.

  • visibleCardsdefault 3
    number

    Cards drawn in the stack, 1 to 4. The rest mount when they reach it; fewer items draw fewer cards.

  • stackdefault "stack"
    "stack" | "fan" | "none"

    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.

  • rotationdefault 12
    number

    Degrees of tilt at a full-width drag, around the bottom of the card. 0 keeps it upright.

  • leftTonedefault "danger"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The reject stamp and button. Stamps always carry text and an icon, so color is never the only cue.

  • rightTonedefault "success"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The accept stamp and button. Brand orange suits a like on a marketplace.

  • upTonedefault "info"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The later stamp and button, when "up" is allowed.

  • radiusdefault "lg"
    "md" | "lg"

    Card radius: md 16px (rounded-2xl), lg 24px (rounded-3xl).

  • showActionsdefault true
    boolean

    The round 56px buttons under the deck: the mouse, keyboard and screen reader path to every decision.

  • showUndodefault true
    boolean

    The undo button next to them. Off also turns off Backspace and Cmd/Ctrl+Z.

  • stampsdefault true
    boolean

    The labels that fade in on the card while it is dragged and lock in when a direction is armed.

  • hapticsdefault true
    boolean

    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.

  • disableddefault false
    boolean

    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).

  • defaultIndexdefault 0
    number

    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.

Dependencies

lucide-reactmotionbuttonempty-stateuse-controllable-statehapticsmotiontonesui-config

Files

  • components/ui/swipe-deck.tsx

FAQ

Is the React Swipe Deck component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Swipe Deck?

Run npx wingo-ui@latest add swipe-deck. It also adds the Wingo UI items it builds on (Button, Empty State, useControllableState, Haptics, Motion presets and 2 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/swipe-deck.

See also

CardEmpty StateHapticslibListSlide To Confirm

More React mobile components

Swipeable TabsHero HeaderBottom Tab BarSticky CTA BarAction SheetFAB
All 15 React mobile components

Articles using this component

  • React Swipe Cards: A Tinder-Style Deck With Undo

    Mobile UI in React · Oct 13, 2026 · 9 min read

PreviousSticky CTA BarNext Swipeable Tabs
Approve expensesNorthwind CRM · September 2026
6 left

Approved: 0·$0.00Rejected: 0

Hilton BostonSep 19, 2026 · F 20931
Total$412.00
LodgingCompany card ····4821
Before tax
$371.17
Tax 11%
$40.83

One night at the Construct Expo fair, no breakfast.

Michael ReedSales
Requested
Amtrak New York–ChicagoSep 18, 2026 · IC 532
Total$156.40
TravelCompany card ····4821
Before tax
$140.90
Tax 11%
$15.50

Ticket for the Greenleaf Pharmacy visit, coach class.

Ryan MitchellSales
Requested
FedExSep 16, 2026 · TRK 7120
Total$86.00
ShippingCompany card ····4821
Before tax
$71.07
Tax 21%
$14.93

Sample return to Summit Logistics, order #1042.

Andrew CollinsLogistics
Requested
Whole Foods MarketSep 15, 2026 · RC 0412
Total$214.50
HospitalityCompany card ····4821
Before tax
$193.24
Tax 11%
$21.26

Coffee and snacks for the Tech Solutions meeting.

Michael ReedSales
Requested
Reject
Approve

Swipe or use the Left and Right arrows. Backspace undoes.

Drag the card with the mouse, or Tab to it and use the arrow keys

directionsleft, rightrotation12°hapticsonindex0