Swipe Deck

A stack of cards you flick left or right; buttons and arrow keys do the same.

  1. A cobalt ceramic cup of coffee on a brushed steel counter in morning light.

    Huila, Colombia

    Stone fruit and brown sugar · washed

Huila, Colombia

In your box: nothing yet

'use client';

import { SwipeDeck } from '@brand-studio/ui';
import { useState } from 'react';
import { beans, cup, pour } from '@/demos/assets';

const lots = [
  { id: 'huila', title: 'Huila, Colombia', asset: cup, body: <p>Stone fruit and brown sugar · washed</p> },
  { id: 'sidama', title: 'Sidama, Ethiopia', asset: pour, body: <p>Jasmine and bergamot · natural</p> },
  { id: 'kirinyaga', title: 'Kirinyaga, Kenya', asset: beans, body: <p>Blackcurrant and cola · washed</p> },
];

export default function SwipeDeckDemo() {
  const [box, setBox] = useState<string[]>([]);
  return (
    <div style={{ width: '100%', maxWidth: 420 }}>
      <SwipeDeck label="Build this month’s box" cards={lots} leftLabel="Skip" rightLabel="Add to box"
        onSwipe={(card, direction) => { if (direction === 'right') setBox(current => current.includes(card.title) ? current : [...current, card.title]); }} />
      <p style={{ margin: '16px 0 0', textAlign: 'center', fontSize: 14, color: 'var(--bs-muted)' }}>In your box: {box.length ? box.join(', ') : 'nothing yet'}</p>
    </div>
  );
}

Installation

pnpm add @brand-studio/ui

Usage

import { SwipeDeck } from '@brand-studio/ui';
<SwipeDeck label="Build this month’s box" cards={lots} leftLabel="Skip" rightLabel="Add to box"
  onSwipe={(card, direction) => direction === 'right' && add(card)} />

Swiped cards go to the back of the deck, so it never runs out. onSwipe receives the card and 'left' or 'right'.

Accessibility

  • Every swipe has a matching button with a visible label, and the arrow keys work once focus is in the deck.
  • The title of the card on top is announced after each choice.
  • Cards under the top one are hidden from screen readers. Reduced motion skips the throw.

API Reference

PropTypeDefault
cardsDeckCard[]–
label?string'Cards'
leftLabel?string'Skip'
rightLabel?string'Keep'
onSwipe?((card: DeckCard, direction: 'left' | 'right') => void)–
className?string''

Still, Deskhand and Hollis are fictional brands. The coffee photographs are original generated artwork. @brand-studio/ui is MIT licensed.