List View
Data-list row renderer — avatar, title, description, meta, extra, and row actions over the same schema and handle as Table.
Plus Registry
List View ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
List View paints a DataListHandle as stacked rows (avatar, title, description, meta, extra, actions) through presentational List. It is the Query List view="list" body — not the presentational List primitive. Prefer Query List view="list" for a full resource index. Compose List View inside Data List Chrome DataListShell when you own the chrome. Job matrix: List Surfaces.
When To Use
- Row-first resource indexes (inbox, orders by title + note) on the same handle as Table — filters, selection, batch, row actions.
- Prefer Query List
view="list"unless you are already composing a custom shell. - Prefer presentational List inside Related List for Object Page members / activity. List View is index chrome, not a card region.
- Prefer Table when operators compare columns, sort, or scan dense fields.
- Do not put a kit batch bar on five related-list members.
Features
| Area | Behavior |
|---|---|
| Body | List.Item + List.Meta from rowLayout field keys or (row) => ReactNode |
| Escape hatch | renderItem(row, ctx, defaultItem) must return a List.Item. Named rows spread chrome props onto List.Item |
| Selection | Opt-in handle selection — same checkboxes as Table |
| Actions | rowActions via Data List View DataListRowActionsCell |
| Infinite | Height-constrained body + cursor pagination virtualizes rows; getRowId is required |
| Loading | Overlay and/or skeleton rows through loading |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/list-view.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/list-view.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/list-view.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/list-view.jsonWith the @f-ui-plus namespace configured in components.json, shadcn add @f-ui-plus/list-view also works.
Runtime npm deps: @tanstack/react-virtual, lucide-react. registryDependencies: @f-ui-plus/data-list-chrome, @f-ui-plus/data-list-internals, @f-ui-plus/data-list-view, open f-ui (i18n, Empty Value Placeholder, Label Tag, Status Tag, Currency Format, Link, List), shadcn button, checkbox, skeleton, tooltip. Wrap the app in QueryClientProvider.
Usage
import { ListView } from "@/components/f-ui/list-view/list-view";
<ListView
rows={handle.data.rows}
schema={schema}
getRowId={(row) => row.id}
rowLayout={{ title: "orderNumber", description: "notes", extra: ["amount"] }}
/>Examples
Schema Rows
Title is an identity link, description wraps, amount sits in extra. Same schema you would pass to Table. Full recipe with filters and batch: Query List view="list".
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { f } from "@/components/f-ui/field-types/catalog";
import { ListView } from "@/components/f-ui/list-view/list-view";
type Order = {
id: string;
orderNumber: string;
notes: string;
amount: number;
};
const ORDERS: Order[] = [
{ id: "1", orderNumber: "ORD-1001", notes: "Rush delivery", amount: 2499 },
{ id: "2", orderNumber: "ORD-1002", notes: "Standard packing", amount: 849.5 },
{ id: "3", orderNumber: "ORD-1003", notes: "Hold for review", amount: 120 },
];
const schema = defineDataListSchema<Order>({
orderNumber: f.link({
label: "Order #",
href: (row) => `#${row.id}`,
}),
notes: f.text({ label: "Notes" }),
amount: f.currency({ label: "Amount", currency: "USD" }),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
});
function ListViewDemoInner() {
const handle = useDataList({
schema,
listCode: "demo-list-view",
data: ORDERS,
getRowId: (row) => row.id,
defaultPageSize: "all",
});
return (
<DataListProvider dataList={handle}>
<ListView
rows={handle.data.rows}
schema={schema}
getRowId={(row) => row.id}
rowLayout={{ title: "orderNumber", description: "notes", extra: ["amount"] }}
/>
</DataListProvider>
);
}
/** Schema + `rowLayout` painted as stacked rows — the QueryList `view="list"` body. */
export function ListViewDemo() {
return (
<QueryClientProvider client={queryClient}>
<ListViewDemoInner />
</QueryClientProvider>
);
}Selection And Row Actions
Checkboxes and Edit / Remove on the same handle contract as Table. Omit selection when the job is browse + Open only.
- ORD-1001Rush delivery
- ORD-1002Standard packing
"use client";
import { PencilIcon, Trash2Icon } from "lucide-react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import type { RowAction } from "@/components/f-ui/data-list-internals/row-actions/row-action-types";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { f } from "@/components/f-ui/field-types/catalog";
import { ListView } from "@/components/f-ui/list-view/list-view";
import { TooltipProvider } from "@/components/ui/tooltip";
type Order = {
id: string;
orderNumber: string;
notes: string;
};
const ORDERS: Order[] = [
{ id: "1", orderNumber: "ORD-1001", notes: "Rush delivery" },
{ id: "2", orderNumber: "ORD-1002", notes: "Standard packing" },
];
const schema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #" }),
notes: f.text({ label: "Notes" }),
});
const ACTIONS: RowAction<Order>[] = [
{
id: "edit",
label: "Edit",
icon: <PencilIcon className="size-3.5" />,
onClick: () => {},
},
{
id: "remove",
label: "Remove",
icon: <Trash2Icon className="size-3.5" />,
variant: "destructive",
placement: "trailing",
confirm: false,
onClick: () => {},
},
];
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
});
function ListViewActionsDemoInner() {
const handle = useDataList({
schema,
listCode: "demo-list-view-actions",
data: ORDERS,
getRowId: (row) => row.id,
defaultPageSize: "all",
features: { selection: true },
});
return (
<DataListProvider dataList={handle}>
<TooltipProvider>
<ListView
rows={handle.data.rows}
schema={schema}
getRowId={(row) => row.id}
selection={handle.selectionEnabled ? handle.selection : undefined}
rowActions={ACTIONS}
rowLayout={{ title: "orderNumber", description: "notes" }}
/>
</TooltipProvider>
</DataListProvider>
);
}
/** Opt-in checkboxes and row actions — same handle contract as Table. */
export function ListViewActionsDemo() {
return (
<QueryClientProvider client={queryClient}>
<ListViewActionsDemoInner />
</QueryClientProvider>
);
}Headless Usage
useList is the density view-model. Data, selection, sort, and filters stay on the handle you already passed to useDataList. Stock ListView chrome is optional — bind density however you want.
density: normal
rows: 1
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { f } from "@/components/f-ui/field-types/catalog";
import { useList } from "@/components/f-ui/list-view/use-list";
type Order = {
id: string;
orderNumber: string;
};
const schema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #" }),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function ListViewHeadlessDemoInner() {
const handle = useDataList({
schema,
listCode: "demo-list-view-headless",
data: [{ id: "1", orderNumber: "ORD-1001" }],
getRowId: (row) => row.id,
defaultPageSize: "all",
});
const { density, setDensity } = useList(handle);
return (
<>
<p>density: {density}</p>
<p>rows: {handle.data.rows.length}</p>
<button type="button" onClick={() => setDensity("compact")}>
compact
</button>
<button type="button" onClick={() => setDensity("normal")}>
normal
</button>
</>
);
}
/** `useList` is the density view-model. Data, selection, and filters stay on the handle. */
export function ListViewHeadlessDemo() {
return (
<QueryClientProvider client={queryClient}>
<ListViewHeadlessDemoInner />
</QueryClientProvider>
);
}Composition
DataListProvider
└── ListView
└── List
└── List.Item (checkbox? · Meta · extra · DataListRowActionsCell)
└── DataListLoadMoreSentinel (cursor + fillHeight | maxHeight)Headless page: useDataListListPage (from Data List Chrome) → DataListShell → ListView.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rows | readonly TData[] | — | Rows to paint. Usually handle.data.rows. |
schema | DataListSchema<TData> | — | Same schema as Table. |
rowLayout | { avatar?, title, description?, meta?, extra? } | — | Field keys or (row) => ReactNode. title required when renderItem is omitted. Title field keys truncate; description field keys wrap. |
renderItem | (row, ctx, defaultItem) => ReactElement | — | Return a List.Item. Wrap defaultItem or replace it. Named rows must spread chrome props onto List.Item (not only ref). |
density | DataListDensity | "normal" | Compact / normal / comfortable. |
selection | UseListSelectionReturn<TData> | — | Opt-in checkboxes. Requires getRowId. |
getRowId | (row, index) => string | — | Required for selection and scroll-infinite. |
rowActions | RowAction<TData>[] | — | Per-row ops. |
rowActionsDisplay | "inline" | "menu" | "inline" | |
rowActionsPresentation | "icon" | "text" | "icon" | |
maxInlineRowActions | number | icon→2, text→3 | |
onRowClick | (row) => void | — | Nested controls must stopPropagation. |
getRowClassName | (row, index) => string | undefined | — | |
loading | { overlay, preserveRows, skeletonRows? } | — | Overlay and/or skeleton rows. |
fillHeight | boolean | false | Flex-parent scrollport. Query List defaults this on. |
dataList | DataListHandle<TData> | — | Required for the scroll-infinite recipe. Omit to keep List View presentational. |
maxHeight | number | string | — | Cap the rows scrollport without a flex parent. |
virtual | ListViewVirtualConfig | — | Overrides for the auto virtual window — not a host-driven virtual switch. |
Hook
useList(handle, options?) returns { density, setDensity }. Options: density, defaultDensity, onDensityChange. The hook adds no data state.