@daformat/react-swipeable-cards

中文文档
1.0.4发布于 2026/05/19Zero-Clause BSDTypeScriptES Module零依赖近 30 天下载 9858

A headless react swipeable cards carousel component, similar to Tinder, with no dependencies

原始使用文档
文档目录16 节
  1. Installation
  2. Demo
  3. Quick start
  4. Styling examples
  5. Component structure
  6. Components
  7. SwipeableCards.Root
  8. SwipeableCards.Cards
  9. SwipeableCards.CardWrapper
  10. SwipeableCards.SwipeLeftButton / SwipeRightButton / SwipeUp…
  11. Hooks
  12. SwipeableCards.useSwipeableCardsContext()
  13. SwipeableCards.useSwipeableCardsStack()
  14. SwipeableCards.useProgrammaticSwipe()
  15. CSS custom properties
  16. Exported types

React swipeable cards

NPM Version NPM Downloads
Follow daformat on GitHub Follow daformat on X

A headless React swipeable cards carousel component, similar to Tinder, with no runtime dependencies. Drag, fling, or programmatically swipe cards left, right, up or down. Cards can be discarded off-screen or sent to the back of the stack to loop forever. Built with the Web Animations API for smooth, momentum-aware animations and pivot-weighted rotation that feels natural under the finger.

Installation

npm install @daformat/react-swipeable-cards
yarn add @daformat/react-swipeable-cards
pnpm add @daformat/react-swipeable-cards
bun add @daformat/react-swipeable-cards
deno add npm:@daformat/react-swipeable-cards

Demo

https://hello-mat.com/design-engineering/component/swipeable-cards

Quick start

import { SwipeableCards } from "@daformat/react-swipeable-cards";

const cards = [
  { id: "1", card: <div className="card">Card 1</div> },
  { id: "2", card: <div className="card">Card 2</div> },
  { id: "3", card: <div className="card">Card 3</div> },
];

export function App() {
  return (
    <SwipeableCards.Root
      cards={cards}
      emptyView={<div>No more cards</div>}
      onSwipe={(direction, cardId) => {
        console.log(`Swiped ${cardId} ${direction}`);
      }}
    >
      <SwipeableCards.Cards />
      <SwipeableCards.SwipeLeftButton>👎</SwipeableCards.SwipeLeftButton>
      <SwipeableCards.SwipeRightButton>👍</SwipeableCards.SwipeRightButton>
    </SwipeableCards.Root>
  );
}

The component is headless: nothing is styled out of the box. You decide how cards look, you decide how the stack is laid out (typically with CSS custom properties exposed by the component, see CSS custom properties).

Styling examples

Given the following jsx:

// full source: https://github.com/daformat/hello-mat/blob/master/pages/design-engineering/component/swipeable-cards.tsx

<SwipeableCards.Root
  cards={[...cards /* omitted for brevity */]}
  className={styles.cards_root}
  data-style={"stacked-offset" /* "stacked-rotation" | "minimal" */}
  swipeStyle={"sendToBack"}
  sendToBackMargin={16}
  loop
>
  <SwipeableCards.Cards
    visibleStackLength={4}
    style={{ aspectRatio: "650 / 400" }}
  />
</SwipeableCards.Root>

Here is how I styled it in the demo

/* full source: https://github.com/daformat/hello-mat/blob/master/components/SwipeableCards/SwipeableCards.module.scss */

.cards_root {
  --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);

  [data-swipeable-cards-cards] {
    display: grid;
    grid-template-columns: 1fr;
    place-content: center;
    position: relative;
    touch-action: none;
    z-index: 1;

    [data-swipeable-cards-card-wrapper] {
      align-content: center;
      align-self: center;
      grid-area: 1 / 1;
      opacity: calc(
        1 - clamp(0, var(--stack-index0) - var(--visible-stack-length), 1)
      );
      touch-action: none;
      transform-origin: center 0;
      transition: all 0.2s var(--ease-out-cubic);
      transition-property: opacity, scale, padding-top, margin-top;
      width: 100%;
      will-change: opacity, scale, padding-top, margin-top, transform;
    }
  }

  &:is([data-style="stacked-offset"], [data-style="stacked-rotation"]) {
    [data-swipeable-cards-cards] {
      [data-swipeable-cards-card-wrapper] {
        scale: calc(
          100% - min(var(--stack-index0), var(--visible-stack-length)) * 10%
        );
      }
    }
  }

  &[data-style="stacked-offset"] {
    [data-swipeable-cards-cards] {
      [data-swipeable-cards-card-wrapper] {
        // Pick whatever vertical step you want between stacked cards.
        --card-top-distance: clamp(16px, 1vw, 32px);
        --p: calc(
          var(--card-top-distance) *
            max(var(--visible-stack-length) - var(--stack-index0), 0)
        );
        --m: calc(
          var(--card-top-distance) *
            (
              var(--visible-stack-length) - max(
                  var(--visible-stack-length) - var(--stack-index0),
                  0
                )
            )
        );
        margin-top: calc(var(--m) * -1);
        padding-top: calc(var(--p));
      }
    }
  }
}

Component structure

/* Provides context to all swipeable card components */
<SwipeableCards.Root cards={cards} emptyView={...}>
  {/* The stack container */}
  <SwipeableCards.Cards>
    {/* Optional: bring your own renderer; otherwise the default renders one CardWrapper per card */}
    {(stack) =>
      stack.map((card) => (
        <SwipeableCards.CardWrapper key={card.id} card={card} />
      ))
    }
  </SwipeableCards.Cards>
  {/* Optional programmatic swipe buttons */}
  <SwipeableCards.SwipeLeftButton />
  <SwipeableCards.SwipeRightButton />
  <SwipeableCards.SwipeUpButton />
  <SwipeableCards.SwipeDownButton />
</SwipeableCards.Root>

Components

SwipeableCards.Root

The outermost wrapper. Provides context, sets up the drag state, and listens to pointermove / pointerup on the document. Renders a <div>.

The loop prop is part of a discriminated union: when loop is false (or omitted) you must pass emptyView; when loop is true you must not pass emptyView.

PropTypeDefaultDescription
cards (required)CardWithId[]—Initial cards in the stack. The last item is the top card.
emptyViewReactNode—Required when loop is false. Displayed when the stack is empty.
loopbooleanfalseWhen true, swiped cards animate to the back of the stack instead of being removed. emptyView cannot be set in this mode.
onSwipe(direction: SwipeDirection, cardId: string) => void—Called once per swipe, immediately after the card commits to flying out (before the animation finishes).
swipeStyle"discard" | "sendToBack""discard""discard" flings the card off-screen and fades it out. "sendToBack" slides it just past the stack edge before animating it under the other cards.
sendToBackMarginnumber0Extra pixels the card travels past the stack edge before being sent to the back. Only used when swipeStyle is "sendToBack".
getCardElement(element: Element) => Elementfirst childReceives the wrapper element and returns the actual card element to use for collision detection. Defaults to element.firstElementChild ?? element, so by default any padding on the wrapper is excluded. Override when the visible card isn't the wrapper's first child (e.g. when you nest extra layout elements inside the wrapper).
swipeDirectionsAllowedSwipeDirection | AllowedSwipeDirection[]all 4Restricts which directions the card can be swiped. Accepts a single value or array of SwipeDirection | "x" | "y" | "horizontal" | "vertical". The shortcuts expand to the corresponding pair ("x"/"horizontal" → ["left", "right"], "y"/"vertical" → ["up", "down"]). Constrains pointer drag to the allowed axes/directions and disables the matching directional buttons.
refRef<HTMLDivElement>—Forwarded ref to the root <div>.
...propsComponentPropsWithoutRef<"div">—All standard <div> props (className, style, children, etc.).

Data attributes set on the root

AttributeValuesDescription
data-swipeable-cards-root""Always present. Marks the root for context lookups.

SwipeableCards.Cards

The stack container. Sets CSS custom properties used by your card styles to compute z-index, scale, opacity, etc. for stacked cards. Renders a <div> plus an inner <div data-empty> that holds the empty view when the stack is empty (the empty view is always rendered, so you can transition it in/out freely).

PropTypeDefaultDescription
visibleStackLengthnumber4How many cards from the top of the stack should appear in the visible peek. Used to compute --visible-stack-length for your styles.
childrenReactNode | (stack: CardWithId[]) => ReactNoderenders one wrapper per cardEither pre-rendered children or a function that receives the current stack and returns nodes. Use the function form when you need per-card customization.
refRef<HTMLDivElement>—Forwarded ref to the cards <div>.
...propsOmit<ComponentPropsWithoutRef<"div">, "children">—All standard <div> props except children (overridden by the function-as-children variant).

Data attributes set on the cards container

AttributeValuesDescription
data-swipeable-cards-cards""Always present. Used internally to locate the stack.
data-empty""Set on the inner wrapper that holds the empty view.

SwipeableCards.CardWrapper

A wrapper around a single card. Owns the pointer interactions for that card — only the top card responds to pointerdown. Renders a <div> containing either card.card or, if you pass children, your own content.

PropTypeDefaultDescription
card (required)CardWithId—The { id, card } entry from the stack this wrapper represents.
childrenReactNodecard.cardOverride the card's own JSX. Useful for layering controls on top of the card while keeping the wrapper handling pointer events.
refRef<HTMLDivElement>—Forwarded ref to the wrapper <div>.
...propsComponentPropsWithoutRef<"div">—All standard <div> props. onPointerDown and onDragStart are merged with internal handlers (yours runs after).

Data attributes set on each wrapper

AttributeValuesDescription
data-swipeable-cards-card-wrapper""Always present. Used internally to enumerate stack items.
data-swipeable-cards-idthe card.idLets you target a specific card from CSS or query selectors.
data-swipeable-cards-top-card"true" | "false""true" when this wrapper is currently the top card. Useful for highlighting the actionable card with an attribute selector.

SwipeableCards.SwipeLeftButton / SwipeRightButton / SwipeUpButton / SwipeDownButton

Programmatic swipe buttons. Clicking one triggers a swipe of the current top card in that direction with a randomized pivot, velocity, and rotation, so repeated clicks feel natural. Automatically no-op when the stack is empty or a swipe is already in progress. Render a <button>.

PropTypeDefaultDescription
onClickMouseEventHandler<HTMLButtonElement>—Called synchronously before the swipe is triggered. Useful for analytics, sound effects, or syncing companion UI animations.
refRef<HTMLButtonElement>—Forwarded ref to the <button>.
...propsComponentPropsWithoutRef<"button">—All standard <button> props.

Hooks

SwipeableCards.useSwipeableCardsContext()

Returns the full internal context. Must be used inside SwipeableCards.Root.

const {
  loop,
  emptyView,
  cards,
  stack,
  setStack,
  discardedCardId,
  setDiscardedCardId,
  dragStateRef,
  animationRef,
  commitSwipe,
  swipeStyle,
  getCardElement,
  sendToBackMargin,
  rootRef,
  swipeDirections,
  swipeDirectionsRef,
} = SwipeableCards.useSwipeableCardsContext();
PropertyTypeDescription
loopboolean | undefinedReflects the loop prop passed to Root.
emptyViewReactNodeThe empty view passed to Root (only set when loop is false).
cardsCardWithId[]The original cards prop passed to Root. Use stack for the live, mutated list.
stackCardWithId[]The current stack. Cards are removed (or rotated, when looping) as they are swiped.
setStackDispatch<SetStateAction<CardWithId[]>>Imperatively replace the stack. Useful for adding new cards on demand.
discardedCardIdstringThe id of the card currently animating out, or "" if no swipe is in progress.
setDiscardedCardIdDispatch<SetStateAction<string>>Setter for the above (rarely needed externally).
dragStateRefRefObject<DraggingState>The mutable drag state (pointer position, velocity, pivot, currently-dragged element). Read it during a drag for custom UI feedback.
animationRefRefObject<Animation[]>The Web Animations API animations currently running for the active swipe.
commitSwipe(manual?: boolean, event?: PointerEvent) => voidCommits the current drag state into a swipe (or returns the card to the stack if the gesture is too small).
swipeStyle"discard" | "sendToBack"Reflects the swipeStyle prop.
getCardElement(element: Element) => ElementReflects the getCardElement prop.
sendToBackMarginnumberReflects the sendToBackMargin prop.
rootRefRefObject<HTMLDivElement>Ref to the root <div> rendered by SwipeableCards.Root.
swipeDirectionsSwipeDirection[]The normalized list of allowed swipe directions (defaults to all 4). Re-renders consumers when the swipeDirections prop changes.
swipeDirectionsRefRefObject<SwipeDirection[]>Ref mirror of the above for read-anywhere access in event handlers without re-binding listeners.

SwipeableCards.useSwipeableCardsStack()

Convenience hook that returns just the current stack (memoized). Use this in components that only care about the stack, to avoid re-rendering on unrelated context changes.

const stack = SwipeableCards.useSwipeableCardsStack();

SwipeableCards.useProgrammaticSwipe()

Powers the built-in directional buttons. Use it to build your own swipe triggers. Throws if the surrounding tree does not include a SwipeableCards.Cards container when the trigger fires.

const { trigger, swipeStyle, swipeDirections } =
  SwipeableCards.useProgrammaticSwipe();
PropertyTypeDescription
trigger(configure: (state: DraggingState, rect: DOMRect) => void) => voidTriggers a swipe of the top card. Your configure callback receives the (zeroed) drag state and the wrapper's bounding rect; set velocityX/Y, pivotX/Y, startX/Y, lastX/Y.
swipeStyle"discard" | "sendToBack"Mirrors the active swipeStyle so your custom button can pick velocity values that look right for either mode.
swipeDirectionsSwipeDirection[]The normalized list of allowed directions, useful for disabling your own custom triggers when their direction is not allowed.

Example: a "super-like" upward swipe

function SuperLikeButton() {
  const { trigger } = SwipeableCards.useProgrammaticSwipe();
  return (
    <button
      type="button"
      onClick={() =>
        trigger((state, rect) => {
          state.velocityX = 0;
          state.velocityY = -(rect.height / 442) * 4;
          state.pivotX = 0;
          state.pivotY = -0.4;
          state.startY = 0;
          state.lastY = -1;
        })
      }
    >
      ⭐
    </button>
  );
}

CSS custom properties

The component sets these CSS custom properties so you can fully drive the visual stack appearance from your own CSS, without prop-drilling values into every card.

On SwipeableCards.Cards

PropertyDescription
--stack-lengthNumber of cards currently in the stack.
--visible-stack-lengthNumber of cards in the "peek" (top of the stack visible behind the topmost card), capped to visibleStackLength.

On each SwipeableCards.CardWrapper

PropertyDescription
--stack-index1-based index of the card from the top. The top card is 1.
--stack-index00-based index of the card from the top. The top card is 0.

Example styling

[data-swipeable-cards-card-wrapper] {
  position: absolute;
  inset: 0;
  transform-origin: center top;
  transition:
    scale 0.3s,
    translate 0.3s;
  scale: calc(1 - var(--stack-index0) * 0.05);
  translate: 0 calc(var(--stack-index0) * clamp(16px, 1vw, 32px));
  z-index: var(--stack-length);
  pointer-events: none;
}

[data-swipeable-cards-top-card="true"] {
  pointer-events: auto;
  cursor: grab;
}

[data-swipeable-cards-top-card="true"]:active {
  cursor: grabbing;
}

Exported types

import type {
  CardWithId,
  SwipeDirection,
  AllowedSwipeDirection,
  SwipeStyle,
  GetCardElement,
  StackRenderer,
  DraggingState,
  BaseSwipeableCardsProps,
  NotLoopingSwipeableProps,
  LoopingSwipeableProps,
  SwipeableCardsProps,
  SwipeableCardsCardsProps,
  SwipeableCardsCardWrapperProps,
} from "@daformat/react-swipeable-cards";
TypeShape
CardWithId{ id: string; card: JSX.Element }
SwipeDirection"left" | "right" | "up" | "down"
AllowedSwipeDirectionSwipeDirection | "x" | "y" | "horizontal" | "vertical" — accepted by the swipeDirections prop.
SwipeStyle"discard" | "sendToBack"
GetCardElement(element: Element) => Element
StackRenderer(stack: CardWithId[]) => ReactNode
DraggingStateMutable drag state object (pointer position, velocity, pivot, currently-dragged element). See dragStateRef above.
SwipeableCardsPropsDiscriminated union of NotLoopingSwipeableProps and LoopingSwipeableProps.

版本与发布记录

@daformat/react-swipeable-cards 共发布 4 个版本,首次发布于 2026/05/03;过去 12 个月发布了 4 个版本。

版本发布日期距今
1.0.4latest2026/05/195 个月前
1.0.32026/05/185 个月前
1.0.22026/05/096 个月前
1.0.12026/05/036 个月前

关于 @daformat/react-swipeable-cards 的常见问题

@daformat/react-swipeable-cards 是什么?

A headless react swipeable cards carousel component, similar to Tinder, with no dependencies

如何安装 @daformat/react-swipeable-cards?

在项目目录运行 npm install @daformat/react-swipeable-cards,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@daformat/react-swipeable-cards 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@daformat/react-swipeable-cards 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@daformat/react-swipeable-cards 可以商用吗?

@daformat/react-swipeable-cards 使用 Zero-Clause BSD 许可。声明的许可为 Zero-Clause BSD,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@daformat/react-swipeable-cards 的下载量有多少?

根据 npm 官方下载统计,@daformat/react-swipeable-cards 在 2026/10/11 – 2026/11/09 这 30 天内被下载 9,858 次,日均约 329 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@daformat/react-swipeable-cards 还在维护吗?

最新版本 1.0.4 发布于 2026/05/19(5 个月前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/05/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。