Layout Composition

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

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/archive-index.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<ArchiveIndex/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/archive-index.en.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 | null—Controlled expanded row id; passing it enters controlled mode
defaultExpandedIdstring | nullnullInitial expanded row id in uncontrolled mode
onExpandedChange(id: string | null) => void—Fires when the expanded row changes (null on collapse)
sortArchiveSort | null—Controlled sort state; passing it enters controlled mode
defaultSortArchiveSort | nullnullInitial 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)
stickyHeaderbooleantrueWhether the header sticks to the top of the scroll container
classNamestring—Appended to the outermost container's className

ArchiveColumn

PropTypeDefaultDescription
keystring—Unique column key, matching a key in ArchiveEntry.fields
headerReact.ReactNode—Header 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)"
classNamestring—Appended to this column's cells

ArchiveEntry

PropTypeDefaultDescription
idstring—Unique 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.ReactNode—Full-width detail content when expanded; when omitted, this row cannot be expanded
labelstring—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 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.