WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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

PropTypeDefaultDescription
columnsArchiveColumn[]Column definitions (year / project / category and so on)
entriesArchiveEntry[]Data rows
expandedIdstring | nullControlled expanded row id; passing it enters controlled mode
defaultExpandedIdstring | nullnullInitial expanded row id in uncontrolled mode
onExpandedChange(id: string | null) => voidFires when the expanded row changes (null on collapse)
sortArchiveSort | nullControlled sort state; passing it enters controlled mode
defaultSortArchiveSort | nullnullInitial sort state in uncontrolled mode
onSortChange(sort: ArchiveSort | null) => voidFires when the sort changes (null when the sort is cleared)
renderDetail(entry: ArchiveEntry) => React.ReactNodeRender prop producing the detail content (takes priority over entry.detail)
stickyHeaderbooleantrueWhether the header sticks to the top of the scroll container
classNamestringAppended to the outermost container's className

ArchiveColumn

PropTypeDefaultDescription
keystringUnique column key, matching a key in ArchiveEntry.fields
headerReact.ReactNodeHeader content
sortablebooleantrueWhether this column can be sorted
align"left" | "right""left"Cell alignment
widthstring"minmax(0,1fr)"CSS grid track width, for example "6rem" or "minmax(0,2fr)"
classNamestringAppended to this column's cells

ArchiveEntry

PropTypeDefaultDescription
idstringUnique row identifier, used for controlled expansion, FLIP pairing, and the React key
fieldsRecord<string, React.ReactNode>Display value per column; keys match ArchiveColumn.key
sortValuesRecord<string, string | number>Sort value per column; falls back to the string / number values in fields when omitted
detailReact.ReactNodeFull-width detail content when expanded; when omitted, this row cannot be expanded
labelstringAccessible 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 original entries order. 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 through sortValues.

Accessibility

  • The header sort controls are native <button> elements, keyboard focusable and operable, with an aria-label naming the sort column.
  • Expandable rows are native <button> elements with aria-expanded and aria-controls pointing at the detail block; the detail block is a role="region" with an aria-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.

On this page