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
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>
| Prop | Type | Default | Description |
|---|---|---|---|
| itemLabel required | (item: T) => string | – | Accessible name of each card, e.g. the job title. |
| items required | T[] | – | |
| onDecision required | (item: T, decision: SwipeDecision) => void | – | |
| renderCard required | (item: T) => ReactNode | – | |
| emptyState | ReactNode | No more cards | |
| leftLabel | string | Pass | |
| rightLabel | string | Interested | |
| threshold | number | 110 | Distance in px that counts as a decision. |