Skip to content

Data display

Swipe deck

Decide on cards one at a time: drag, buttons or arrow keys.

import { SwipeDeck } from '@jsonresume/ui'Source(opens in a new tab)

Examples

Job matches

Stripe

Senior Engineer

$180k–$220k

GoKafka

Drag a card, use the buttons, or focus the deck and press ← / →.

Show code
swipe-deck/jobs.tsx
'use client';

import { Badge, Stack, SwipeDeck, Text } from '@jsonresume/ui';
import { useState } from 'react';
import * as styles from './jobs.css';

const jobs = [
  {
    id: 1,
    title: 'Senior Engineer',
    company: 'Stripe',
    salary: '$180k–$220k',
    tags: ['Go', 'Kafka'],
  },
  {
    id: 2,
    title: 'Staff Frontend Engineer',
    company: 'Vercel',
    salary: '$200k+',
    tags: ['React', 'Next.js'],
  },
  {
    id: 3,
    title: 'Platform Engineer',
    company: 'Fly.io',
    salary: '$170k',
    tags: ['Rust', 'Linux'],
  },
];

export default function SwipeDeckJobs() {
  const [log, setLog] = useState<string[]>([]);
  return (
    <Stack gap={4} align="center" className={styles.root}>
      <SwipeDeck
        items={jobs}
        itemLabel={(j) => `${j.title} at ${j.company}`}
        onDecision={(j, d) =>
          setLog((l) => [`${d === 'right' ? '✓' : '✕'} ${j.company}`, ...l])
        }
        renderCard={(j) => (
          <Stack gap={3} className={styles.card}>
            <Text caps tone="muted">
              {j.company}
            </Text>
            <Text size="lg" weight="semibold">
              {j.title}
            </Text>
            <Text tone="muted">{j.salary}</Text>
            <Stack direction="row" gap={2}>
              {j.tags.map((t) => (
                <Badge key={t} tone="outline">
                  {t}
                </Badge>
              ))}
            </Stack>
          </Stack>
        )}
        emptyState="You're all caught up. New matches arrive daily."
      />
      <Text size="xs" tone="muted">
        {log.join('  ') ||
          'Drag a card, use the buttons, or focus the deck and press ← / →.'}
      </Text>
    </Stack>
  );
}
swipe-deck/jobs.css.ts
import { style } from '@vanilla-extract/css';
import { vars } from '@jsonresume/ui/tokens';

export const root = style({ width: '100%' });

export const card = style({ padding: vars.space[6] });

Accessibility

  • Every decision has a button; dragging is never the only way.
  • The deck is focusable, arrow keys decide, and decisions are announced.

Props

<SwipeDeck>

Also accepts the attributes of the element SwipeDeck renders.
PropTypeDefaultDescription
itemLabel required(item: T) => string–Accessible name of each card, e.g. the job title.
items requiredT[]–
onDecision required(item: T, decision: SwipeDecision) => void–
renderCard required(item: T) => ReactNode–
emptyStateReactNodeNo more cards
leftLabelstringPass
rightLabelstringInterested
thresholdnumber110Distance in px that counts as a decision.