Build / Molecules
CardIndex
The index to a CardPile — everyone in it, and a way to bring any one of
them to the top.
A pile hands you one card at a time, which is what makes it a pile and not a directory; the cost is that finding a particular person means flipping past everyone in front of them. This is the contents page: it says who is in the pile without opening it, and it takes you straight to one.
The card already on top stays in the list, marked aria-current, rather
than being filtered out of it. Two reasons, and both are about the list
holding still: a list of "the others" reshuffles every time you advance,
so the row you were about to click moves out from under you; and with the
current card in place the list is also a position indicator — where you
are in the pile, not just what else is in it.
import { CardIndex } from '@meetcard/deck'Example
<CardIndex label="Everyone from Founders Dinner" value={cards[index].slug} onValueChange={(id) => setIndex(cards.findIndex((c) => c.slug === id))} items={cards.map((c) => ({ id: c.slug, name: c.name, detail: c.title }))} />Stories
Rows: who is in the pile and what they do. The card on top of the pile stays in the list rather than being filtered out of it — so the list holds still while you flip through, and doubles as "where am I".
Source
const Default = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{ id: 'sam', name: 'Sam Ellery', detail: 'CEO at Trail & Co' },
{ id: 'omar', name: 'Omar Whitfield', detail: 'Operating Partner at Halden Group' },
{ id: 'nora', name: 'Nora Lindqvist', detail: 'Creative Director at Field Studio' },
]}
label="Everyone from Founders Dinner"
value="sam"
onValueChange={fn()}
layout="rows" />
</div>;Picking someone asks for their card, by id.
Source
const PickingSomeone = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{ id: 'sam', name: 'Sam Ellery', detail: 'CEO at Trail & Co' },
{ id: 'omar', name: 'Omar Whitfield', detail: 'Operating Partner at Halden Group' },
{ id: 'nora', name: 'Nora Lindqvist', detail: 'Creative Director at Field Studio' },
]}
label="Everyone from Founders Dinner"
value="sam"
onValueChange={fn()}
layout="rows" />
</div>;Strip: faces, scrolled sideways. The index as a way back to a card rather than something to read — so the titles come off and the faces get bigger.
Source
const Strip = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{ id: 'sam', name: 'Sam Ellery', detail: 'CEO at Trail & Co' },
{ id: 'omar', name: 'Omar Whitfield', detail: 'Operating Partner at Halden Group' },
{ id: 'nora', name: 'Nora Lindqvist', detail: 'Creative Director at Field Studio' },
]}
label="Everyone from Founders Dinner"
value="sam"
onValueChange={fn()}
layout="strip" />
</div>;A pile of one: the index has nothing to index, and says so by being one row.
Source
const OneCard = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={items.slice(0, 1)}
label="Everyone from Founders Dinner"
value="sam"
onValueChange={fn()}
layout="rows" />
</div>;Wired to a pile. This is the whole point of the component — the list is the way back to a card you have already flipped past.
Source
const SelectingKeepsTheListStill = () => {
const [value, setValue] = useState('sam')
return (
<div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{ id: 'sam', name: 'Sam Ellery', detail: 'CEO at Trail & Co' },
{ id: 'omar', name: 'Omar Whitfield', detail: 'Operating Partner at Halden Group' },
{ id: 'nora', name: 'Nora Lindqvist', detail: 'Creative Director at Field Studio' },
]}
label="Everyone from Founders Dinner"
layout="rows"
value={value}
onValueChange={setValue} />
</div>
);
};A row can carry more than a name: a pill saying which kind of card it is, and one control of its own.
The control is a sibling of the selecting button rather than a child of it — a button inside a button is not a thing a browser will render — so a row can both "show this one" and "edit this one" without the two fighting over the same click.
Source
const WithBadgesAndActions = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{
id: 'sam',
name: 'Sam Ellery',
detail: 'CEO at Trail & Co',
badge: <Badge tone="neutral" size="sm">Business</Badge>,
action: (
<IconButton label="Edit business card" icon={<PencilIcon />} size="sm" />
),
},
{
id: 'omar',
name: 'Omar Whitfield',
detail: 'Personal profile',
badge: <Badge tone="neutral" size="sm">Personal</Badge>,
action: (
<IconButton label="Edit personal card" icon={<PencilIcon />} size="sm" />
),
},
]}
label="All cards"
value="sam"
onValueChange={fn()}
layout="rows" />
</div>;The marker takes the page's own word for it.
Source
const ANamedCurrentMarker = () => <div style={{ maxWidth: 420 }}>
<CardIndex
items={[
{ id: 'sam', name: 'Sam Ellery', detail: 'CEO at Trail & Co' },
{ id: 'omar', name: 'Omar Whitfield', detail: 'Operating Partner at Halden Group' },
{ id: 'nora', name: 'Nora Lindqvist', detail: 'Creative Director at Field Studio' },
]}
label="Everyone from Founders Dinner"
value="omar"
onValueChange={fn()}
layout="rows"
currentLabel="On top" />
</div>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | CardIndexItem[] | — | — |
currentLabel | string | 'Showing' | What the current row is marked with, in words. Default "Showing" — the card that is on the pile right now. |
label | string | 'Cards in this pile' | Accessible name for the list, e.g. "Everyone from Founders Dinner". |
layout | 'rows' | 'strip' | 'responsive' | 'responsive' | — |
onValueChange | (id: string) => void | — | Fires with the id of the card that should come to the top. |
value | string | — | Which card is on top of the pile, by id. |
Also accepts everything from Omit, so id, data-* and event handlers pass straight through.