Archive Index
A studio-archive-style full-page index table — multiple sortable columns that reorder with FLIP, and rows that expand into full-width detail while the rest elastically make way.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/archive-index.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ArchiveIndex />
Installation
npx shadcn@latest add "https://webberui.com/r/archive-index.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/archive-index.
Usage
import {
ArchiveIndex,
type ArchiveColumn,
type ArchiveEntry,
} from "@/components/ui/archive-index";
const columns: ArchiveColumn[] = [
{ key: "year", header: "Year", width: "4rem", align: "right" },
{ key: "project", header: "Project", width: "minmax(0,1.6fr)" },
{ key: "category", header: "Category" },
];
const entries: ArchiveEntry[] = [
{
id: "aperture",
label: "Aperture identity system",
fields: { year: "2024", project: "Aperture identity system", category: "Branding" },
detail: <p>A complete visual identity for an optical instrument maker.</p>,
},
// ...
];
<ArchiveIndex columns={columns} entries={entries} />Both sorting and expansion support controlled and uncontrolled modes. Passing sort / expandedId enters controlled mode, with the state written back through onSortChange / onExpandedChange:
const [sort, setSort] = React.useState<ArchiveSort | null>({
key: "year",
direction: "desc",
});
<ArchiveIndex
columns={columns}
entries={entries}
sort={sort}
onSortChange={setSort}
/>Props
ArchiveIndex
| Prop | Type | Default | Description |
|---|---|---|---|
columns | ArchiveColumn[] | — | Column definitions (year / project / category and so on) |
entries | ArchiveEntry[] | — | Data rows |
expandedId | string | null | — | Controlled expanded row id; passing it enters controlled mode |
defaultExpandedId | string | null | null | Initial expanded row id in uncontrolled mode |
onExpandedChange | (id: string | null) => void | — | Fires when the expanded row changes (null on collapse) |
sort | ArchiveSort | null | — | Controlled sort state; passing it enters controlled mode |
defaultSort | ArchiveSort | null | null | Initial sort state in uncontrolled mode |
onSortChange | (sort: ArchiveSort | null) => void | — | Fires when the sort changes (null when the sort is cleared) |
renderDetail | (entry: ArchiveEntry) => React.ReactNode | — | Render prop producing the detail content (takes priority over entry.detail) |
stickyHeader | boolean | true | Whether the header sticks to the top of the scroll container |
className | string | — | Appended to the outermost container's className |
ArchiveColumn
| Prop | Type | Default | Description |
|---|---|---|---|
key | string | — | Unique column key, matching a key in ArchiveEntry.fields |
header | React.ReactNode | — | Header content |
sortable | boolean | true | Whether this column can be sorted |
align | "left" | "right" | "left" | Cell alignment |
width | string | "minmax(0,1fr)" | CSS grid track width, for example "6rem" or "minmax(0,2fr)" |
className | string | — | Appended to this column's cells |
ArchiveEntry
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique row identifier, used for controlled expansion, FLIP pairing, and the React key |
fields | Record<string, React.ReactNode> | — | Display value per column; keys match ArchiveColumn.key |
sortValues | Record<string, string | number> | — | Sort value per column; falls back to the string / number values in fields when omitted |
detail | React.ReactNode | — | Full-width detail content when expanded; when omitted, this row cannot be expanded |
label | string | — | Accessible row name, applied to the expand button's aria-label (falls back to id when omitted) |
How it works
- Sort cycle: clicking the same column header cycles through
asc → desc → clear; clearing returns to the originalentriesorder. The sort is stable and never mutates the array you pass in. - FLIP reordering: when the sort changes, rows swap places smoothly with a FLIP animation via
layout="position", on a spring with a slight spring back. - Expand and make way: clicking a row springs its detail open into a full-width block by height, and the rows below are elastically pushed down by the document flow; at the same time the other rows dim slightly to focus the current one.
- Sort values: number against number uses numeric comparison, everything else uses a numeric-aware
localeCompare, so"2019"and"2021"— or year numbers — sort correctly. If a display value is not plain text (a styled node, say), supply a comparable value throughsortValues.
Accessibility
- The header sort controls are native
<button>elements, keyboard focusable and operable, with anaria-labelnaming the sort column. - Expandable rows are native
<button>elements witharia-expandedandaria-controlspointing at the detail block; the detail block is arole="region"with anaria-label. - When the user has "reduce motion" enabled at the system level, the FLIP, height, and dimming animations are disabled and expand / collapse switches instantly, with the DOM structure unchanged.
Parallel Text Columns
A side-by-side comparison layout — bilingual or two-version content on facing columns, paired at the sentence level, auto-aligned on scroll with linked hover highlighting.
String Pinboard
A red-string pinboard layout — cards are clues pinned to corkboard, connected by red strings that sag naturally, with slack and droop changing live as you drag.