# List view

> A list of records: list views, search and a table to sort and select. Lightning's object home, for a standard or custom object.

A shadcn/ui block from the [@wisty](https://ui.wisty.ai/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/wisty/list-view).

- Page, with a live preview: https://shadcn.exchange/r/wisty/list-view
- Screenshot: https://shadcn.exchange/shot/wisty/list-view/light.png
- Registry item: https://ui.wisty.ai/r/new-york-v4/list-view.json
- Categories: list
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/wisty/list-view.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@wisty/tokens`, `@wisty/data-table`, `@wisty/list-view-header`

## Files

### `blocks/list-view.tsx`

```tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  useDataTable,
  type DataTableColumn,
  type DataTableInstance,
  type UseDataTableOptions,
} from "@/components/wisty/data-table"

interface UseListViewOptions<TRecord extends object> extends Omit<
  UseDataTableOptions<TRecord>,
  "data"
> {
  /** The rows the app has read, or `null` when they couldn't load. */
  records: TRecord[] | null
  /** The fields the search matches; leave it out to search on the server. */
  searchBy?: readonly (keyof TRecord)[]
  /** The first read: no count while it lasts, and never unavailable. */
  loading?: boolean
}

interface UseListViewResult<TRecord extends object> {
  /** The table for DataTable, over the rows the search leaves. */
  table: DataTableInstance<TRecord>
  /** The search text, for ListViewHeader's `search`. */
  search: string
  /** Sets the search text, for ListViewHeader's `onSearchChange`. */
  setSearch: (search: string) => void
  /** How many rows the search leaves; `null` while loading or unavailable. */
  itemCount: number | null
  /** The label of the column sorted first, if any, for ListViewHeader's `sortedBy`. */
  sortedBy: string | undefined
  /** The rows couldn't load: `records` is `null` and nothing is loading. */
  unavailable: boolean
}

const none: never[] = []

function columnLabel<TRecord extends object>(column: DataTableColumn<TRecord>) {
  const { meta, header } = column.columnDef
  return meta?.label ?? (typeof header === "string" ? header : column.id)
}

/** Holds the list's search and derives its count, sort label and unavailable state from the app's rows. */
function useListView<TRecord extends object>(
  options: UseListViewOptions<TRecord>
): UseListViewResult<TRecord> {
  const { records, searchBy, loading = false, ...tableOptions } = options
  const [search, setSearch] = React.useState("")
  const query = search.trim().toLowerCase()
  const rows =
    records && searchBy && query
      ? records.filter((record) =>
          searchBy.some((field) =>
            String(record[field] ?? "")
              .toLowerCase()
              .includes(query)
          )
        )
      : (records ?? none)
  const table = useDataTable({ ...tableOptions, data: rows })
  const sort = table.state.sorting[0]
  const sorted = sort ? table.getColumn(sort.id) : undefined
  const unavailable = records === null && !loading

  return {
    table,
    search,
    setSearch,
    itemCount: loading || unavailable ? null : rows.length,
    sortedBy: sorted ? columnLabel(sorted) : undefined,
    unavailable,
  }
}

/** The frame that joins a list view header to the table under it. */
function ListView({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="list-view"
      className={cn(
        "flex flex-col overflow-hidden rounded-(--page-header-radius) border-(length:--page-header-border-width) border-b-0 border-(--page-header-border) shadow-(--page-header-shadow)",
        className
      )}
      {...props}
    />
  )
}

export { ListView, useListView }
export type { UseListViewOptions, UseListViewResult }
```

### `blocks/list-view-page.tsx`

```tsx
"use client"

import { useState } from "react"

import {
  createDataTableColumnHelper,
  DataTable,
  type DataTableRowAction,
} from "@/components/wisty/data-table"
import { ListView, useListView } from "@/components/wisty/list-view"
import {
  ListViewHeader,
  type ListViewHeaderAction,
} from "@/components/wisty/list-view-header"
import {
  listViewAccounts,
  listViewAccountViews,
  listViewRecentAccountViews,
  type ListViewAccount,
} from "@/components/wisty/list-view-data"

const col = createDataTableColumnHelper<ListViewAccount>()
const columns = col.columns([
  col.accessor("name", {
    header: "Account Name",
    minSize: 220,
    meta: { rowHeader: true },
    cell: ({ row, getValue }) => (
      <a
        href={row.original.href}
        className="text-link hover:underline focus-visible:underline"
      >
        {getValue()}
      </a>
    ),
  }),
  col.accessor("phone", {
    header: "Phone",
    size: 160,
    meta: { type: "phone" },
  }),
  col.accessor("type", { header: "Type", size: 170 }),
  col.accessor("owner", { header: "Account Owner", size: 150 }),
  col.accessor("lastModified", {
    header: "Last Modified Date",
    size: 210,
    meta: { type: "datetime" },
  }),
])

const actions: ListViewHeaderAction[] = [
  { value: "new", label: "New" },
  { value: "import", label: "Import" },
  { value: "printable-view", label: "Printable View" },
]

const rowActions: DataTableRowAction[] = [
  { value: "edit", label: "Edit" },
  { value: "delete", label: "Delete" },
]

/** The Accounts list view, over the sample org's accounts. */
function ListViewPage() {
  const [accounts, setAccounts] = useState(listViewAccounts.slice(0, 10))
  const [view, setView] = useState("AllAccounts")
  const hasMore = accounts.length < listViewAccounts.length
  const list = useListView({
    records: accounts,
    searchBy: ["name", "phone", "type", "owner"],
    columns,
    getRowId: (account) => account.id,
    initialState: { sorting: [{ id: "name", desc: false }] },
    showRowNumberColumn: true,
    enableRowSelection: true,
    rowActions,
    locale: "en-US",
    timeZone: "America/Chicago",
  })

  return (
    <ListView className="w-full">
      <ListViewHeader
        icon="standard:account"
        objectLabel="Accounts"
        views={listViewAccountViews}
        recentViews={listViewRecentAccountViews}
        view={view}
        onViewChange={setView}
        itemCount={list.itemCount}
        hasMoreItems={hasMore}
        sortedBy={list.sortedBy}
        filteredBy="All accounts"
        updated={list.unavailable ? undefined : "a few seconds ago"}
        actions={actions}
        search={list.search}
        onSearchChange={list.setSearch}
        locale="en-US"
        joined
      />
      <DataTable
        table={list.table}
        aria-label={
          listViewAccountViews.find((option) => option.value === view)?.label
        }
        hasNextPage={hasMore}
        onLoadMore={() => setAccounts(listViewAccounts)}
        empty={list.unavailable ? "Accounts aren't available." : undefined}
      />
    </ListView>
  )
}

export { ListViewPage }
```

### `blocks/list-view-data.ts`

```ts
import type { ListViewHeaderView } from "@/components/wisty/list-view-header"

/** One account in the All Accounts list view, its columns as plain values. */
export interface ListViewAccount {
  /** The account's Id. */
  id: string
  /** The account's name, linked to its record page. */
  name: string
  /** The account's phone, or null. */
  phone: string | null
  /** The account's Type picklist value, as labeled. */
  type: string | null
  /** The account owner's name. */
  owner: string
  /** Last Modified Date, an ISO 8601 instant. */
  lastModified: string
  /** The account's record page. */
  href: string
}

/** The sample org's accounts, as the All Accounts list view shows them. */
export const listViewAccounts: ListViewAccount[] = [
  {
    id: "001Dn00000BurlTX",
    name: "Burlington Textiles Corp of America",
    phone: "(336) 222-7000",
    type: "Customer - Direct",
    owner: "Ellen Hart",
    lastModified: "2026-06-12T15:42:00Z",
    href: "/accounts/001Dn00000BurlTX",
  },
  {
    id: "001Dn00000DickPL",
    name: "Dickenson plc",
    phone: "(785) 241-6200",
    type: "Customer - Channel",
    owner: "Ellen Hart",
    lastModified: "2026-06-03T18:10:00Z",
    href: "/accounts/001Dn00000DickPL",
  },
  {
    id: "001Dn00000EdgeCOM",
    name: "Edge Communications",
    phone: "(512) 757-6000",
    type: "Customer - Direct",
    owner: "Ellen Hart",
    lastModified: "2026-06-12T14:05:00Z",
    href: "/accounts/001Dn00000EdgeCOM",
  },
  {
    id: "001Dn00000ExprLG",
    name: "Express Logistics and Transport",
    phone: "(503) 421-7800",
    type: "Customer - Channel",
    owner: "Priya Raman",
    lastModified: "2026-05-29T20:31:00Z",
    href: "/accounts/001Dn00000ExprLG",
  },
  {
    id: "001Dn00000GenePT",
    name: "GenePoint",
    phone: "(650) 867-3450",
    type: "Customer - Channel",
    owner: "Priya Raman",
    lastModified: "2026-06-10T16:48:00Z",
    href: "/accounts/001Dn00000GenePT",
  },
  {
    id: "001Dn00000GrndHR",
    name: "Grand Hotels & Resorts Ltd",
    phone: "(312) 596-1000",
    type: "Customer - Direct",
    owner: "Ellen Hart",
    lastModified: "2026-06-08T13:22:00Z",
    href: "/accounts/001Dn00000GrndHR",
  },
  {
    id: "001Dn00000PyrmCN",
    name: "Pyramid Construction Inc.",
    phone: "(014) 427-4427",
    type: "Customer - Channel",
    owner: "Priya Raman",
    lastModified: "2026-05-21T09:15:00Z",
    href: "/accounts/001Dn00000PyrmCN",
  },
  {
    id: "001Dn00000SfrcIN",
    name: "sForce",
    phone: "(415) 901-7000",
    type: null,
    owner: "Ellen Hart",
    lastModified: "2026-04-30T17:02:00Z",
    href: "/accounts/001Dn00000SfrcIN",
  },
  {
    id: "001Dn00000UogCRP",
    name: "United Oil & Gas Corp.",
    phone: "(212) 842-5500",
    type: "Customer - Direct",
    owner: "Ellen Hart",
    lastModified: "2026-06-11T19:40:00Z",
    href: "/accounts/001Dn00000UogCRP",
  },
  {
    id: "001Dn00000UogSGP",
    name: "United Oil & Gas, Singapore",
    phone: "(650) 450-8810",
    type: "Customer - Direct",
    owner: "Priya Raman",
    lastModified: "2026-06-02T03:55:00Z",
    href: "/accounts/001Dn00000UogSGP",
  },
  {
    id: "001Dn00000UogUKL",
    name: "United Oil & Gas, UK",
    phone: "+44 191 4956203",
    type: "Customer - Direct",
    owner: "Priya Raman",
    lastModified: "2026-05-26T11:08:00Z",
    href: "/accounts/001Dn00000UogUKL",
  },
  {
    id: "001Dn00000UnivAZ",
    name: "University of Arizona",
    phone: "(520) 773-9050",
    type: "Customer - Direct",
    owner: "Ellen Hart",
    lastModified: "2026-06-09T22:17:00Z",
    href: "/accounts/001Dn00000UnivAZ",
  },
]

/** The org's Accounts list views, in the switcher's order. */
export const listViewAccountViews: ListViewHeaderView[] = [
  { value: "AllAccounts", label: "All Accounts" },
  { value: "MyAccounts", label: "My Accounts" },
  { value: "NewThisWeek", label: "New This Week" },
  {
    value: "PlatinumandGoldSLACustomers",
    label: "Platinum and Gold SLA Customers",
  },
  { value: "RecentlyViewedAccounts", label: "Recently Viewed Accounts" },
]

/** The user's recently used Accounts list views, most recent first. */
export const listViewRecentAccountViews = ["AllAccounts", "MyAccounts"]
```
