Data List Chrome
View-agnostic list regions — shell, toolbar, search, pagination, empty, error, selection bar, and filter popover — composed around one handle.
Plus Registry
Data List Chrome ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
Data List Chrome is the L2 region kit around a DataListHandle: layout shell, toolbar, search, pagination, empty / error, selection / batch, and column-header filters. Query List is the assembled recipe that already wires these regions. Reach for this package when you are composing a custom list shell — not when Query List already fits.
Body painters stay separate: Table or List View. Shared cells live in Data List View. Job matrix: List Surfaces.
When To Use
- You need a list page that Query List cannot express (custom stats stack, unusual footer, mixed bodies) and will compose
DataListProvider→DataListShellyourself. - Prefer Query List for a standard resource index (Query Filter + toolbar + table or list + pagination + batch).
- Do not use this chrome inside an Object Page card — that is Related List.
- Do not put schema, selection, or batch on presentational List.
- Filter-panel rail and column-header filters are documented on Filter Panel / Table — Composition.
Features
| Area | Behavior |
|---|---|
| Shell | DataListShell — stats, toolbar, optional sidebar filters, fullscreen, footer |
| Toolbar | DataListToolbar — title, pill tabs, actions, search, Refresh, tools, filters trigger |
| Query hook | useDataListQueryPage / useDataListListPage bind a handle + view-model for table or list bodies |
| Navigation | DataListPagination, DataListLoadMore, DataListLoadMoreSentinel |
| Status | DataListEmpty (no rows vs no matches), DataListErrorState, DataListFailureSummary |
| Selection | DataListSelectionBar, DataListFooterToolbar (docks via PageFooter), DataListBatchActionButton |
| Filters | DataListSearchInput, DataListActiveFilters, DataListColumnFilter, FilterPanel |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-chrome.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-chrome.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-chrome.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-chrome.jsonWith the @f-ui-plus namespace configured in components.json, shadcn add @f-ui-plus/data-list-chrome also works.
Runtime npm dep: lucide-react. The CLI also pulls @f-ui-plus/page, @f-ui-plus/data-list-internals, @f-ui-plus/table, @f-ui-plus/list-view, open f-ui peers (Result, i18n, date / number / search inputs, Confirm, Empty Value Placeholder, Multi-Select, Currency), and shadcn primitives (button, checkbox, input, popover, select, sheet, …). Wrap the app in QueryClientProvider.
Usage
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListShell } from "@/components/f-ui/data-list-chrome/layout/data-list-shell";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { DataListPagination } from "@/components/f-ui/data-list-chrome/data-list-pagination";
const handle = useDataList({ schema, listCode: "orders", data: rows, getRowId: (r) => r.id });
<DataListProvider dataList={handle}>
<DataListShell
toolbar={<DataListToolbar title="Orders" />}
footer={<DataListPagination />}
>
<Table dataList={handle} />
</DataListShell>
</DataListProvider>Headless query-list idiom: useDataListQueryPage → spread tableViewProps onto TableView and pass slots into the shell. See Table — Composition and Query List.
Examples
Custom Shell
Toolbar title, host-driven pill tabs, keyword search, selection bar, and offset pagination around Table. Try search, a tab, and a checkbox. Prefer Query List when you do not need this level of assembly.
| ORD-1001 | Acme Corp | Open | 2,499.00 | |
| ORD-1002 | Globex Inc | Shipped | 849.50 | |
| ORD-1003 | Initech | Open | 120.00 |
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import { DataListActiveFilters } from "@/components/f-ui/data-list-chrome/data-list-active-filters";
import { DataListPagination } from "@/components/f-ui/data-list-chrome/data-list-pagination";
import { DataListSearchInput } from "@/components/f-ui/data-list-chrome/data-list-search-input";
import { DataListRefresh } from "@/components/f-ui/data-list-chrome/data-list-refresh";
import { DataListSelectionBar } from "@/components/f-ui/data-list-chrome/data-list-selection-bar";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { DataListToolbarTabs } from "@/components/f-ui/data-list-chrome/data-list-toolbar-tabs";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListShell } from "@/components/f-ui/data-list-chrome/layout/data-list-shell";
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 { Table } from "@/components/f-ui/table/table";
type Order = {
id: string;
orderNumber: string;
customer: string;
status: "open" | "shipped";
amount: number;
};
const ORDERS: Order[] = [
{ id: "1", orderNumber: "ORD-1001", customer: "Acme Corp", status: "open", amount: 2499 },
{ id: "2", orderNumber: "ORD-1002", customer: "Globex Inc", status: "shipped", amount: 849.5 },
{ id: "3", orderNumber: "ORD-1003", customer: "Initech", status: "open", amount: 120 },
{ id: "4", orderNumber: "ORD-1004", customer: "Umbrella", status: "shipped", amount: 640 },
];
const schema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #", sortable: true }),
customer: f.text({ label: "Customer" }),
status: f.enum({
render: "status",
label: "Status",
variants: {
open: { label: "Open", tone: "warning" },
shipped: { label: "Shipped", tone: "info" },
},
}),
amount: f.currency({ label: "Amount", currency: "USD", sortable: true }),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const TABS = [
{ id: "all", label: "All" },
{ id: "open", label: "Open" },
{ id: "shipped", label: "Shipped" },
];
function DataListChromeDemoInner() {
const [tab, setTab] = useState("all");
const handle = useDataList({
schema,
listCode: "demo-data-list-chrome",
data: tab === "all" ? ORDERS : ORDERS.filter((row) => row.status === tab),
getRowId: (row) => row.id,
defaultPageSize: 3,
features: { filters: true, selection: true, pagination: { mode: "offset" } },
});
return (
<DataListProvider dataList={handle}>
<DataListShell
className="h-[min(480px,70vh)] min-h-80 gap-0 overflow-hidden rounded-xl border bg-card p-4"
toolbar={
<DataListToolbar
title="Orders"
tabs={<DataListToolbarTabs tabs={TABS} value={tab} onChange={setTab} />}
toolsStart={<DataListSearchInput className="sm:w-56" />}
refresh={<DataListRefresh />}
/>
}
footer={<DataListPagination className="border-border border-t pt-2" />}
>
<DataListActiveFilters className="mb-2 shrink-0" />
<DataListSelectionBar variant="embedded" className="mb-2 rounded-none border-0" />
<Table dataList={handle} fillHeight columns={{ amount: { align: "end" } }} />
</DataListShell>
</DataListProvider>
);
}
/** Host-composed list shell: toolbar, search, selection, pagination around Table. */
export function DataListChromeDemo() {
return (
<QueryClientProvider client={queryClient}>
<DataListChromeDemoInner />
</QueryClientProvider>
);
}Empty And Error
DataListEmpty is a successful empty set (inbox vs no-matches when filters are active). DataListErrorState is a failed fetch with Retry — never collapse error into empty. Page/region triad: Page And Region Status.
Something went wrong
Could not load orders.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DataListEmpty } from "@/components/f-ui/data-list-chrome/data-list-empty";
import { DataListErrorState } from "@/components/f-ui/data-list-chrome/data-list-error-state";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListShell } from "@/components/f-ui/data-list-chrome/layout/data-list-shell";
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";
type Order = {
id: string;
orderNumber: string;
};
const schema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #" }),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function EmptyShell() {
const handle = useDataList({
schema,
listCode: "demo-data-list-chrome-empty",
data: [] as Order[],
getRowId: (row) => row.id,
features: { filters: true },
});
return (
<DataListProvider dataList={handle}>
<DataListShell
className="min-h-56 overflow-hidden rounded-xl border bg-card p-4"
toolbar={<DataListToolbar title="Orders" />}
>
<DataListEmpty />
</DataListShell>
</DataListProvider>
);
}
/** Empty vs fetch-error chrome. Empty is a successful empty set; Error needs Retry. */
export function DataListChromeStatesDemo() {
return (
<QueryClientProvider client={queryClient}>
<div className="grid gap-4 md:grid-cols-2">
<EmptyShell />
<DataListShell className="min-h-56 overflow-hidden rounded-xl border bg-card p-4">
<DataListErrorState
error={new Error("Could not load orders.")}
onRetry={() => {}}
/>
</DataListShell>
</div>
</QueryClientProvider>
);
}Composition
DataListProvider (handle → context)
└── DataListShell
├── stats?
├── toolbar → DataListToolbar
│ ├── tabs → DataListToolbarTabs
│ ├── actions
│ ├── toolsStart → DataListSearchInput
│ └── tools (column manager, density; filtersTrigger from the shell)
├── filters? → FilterPanel (sidebar / sheet)
├── body → Table | ListView
│ ├── DataListActiveFilters
│ └── DataListSelectionBar (or above the body)
└── footer
├── DataListPagination | DataListLoadMore
└── DataListFooterToolbar (PageFooter when inside PageContainer)API Reference
Public entrypoints — props below are the ones hosts set. Filter field editors and operator popovers are internals of DataListColumnFilter / FilterPanel.
Layout
| Export | Role |
|---|---|
DataListProvider | Publishes the handle. Locks chrome controls while the list is interaction-locked — not a full-tree scrim. |
DataListShell | toolbar, stats, footer, children, optional filters rail, fullscreen. renderToolbar({ isFullscreen, toggle, filtersTrigger }) when the toolbar must react to fullscreen. |
useDataListQueryPage | { handle, view, providerProps, tableViewProps, slots, selectedRows } for table-bodied pages. |
useDataListListPage | { handle, view, providerProps, slots, selectedRows } for list-bodied pages (view is useList). |
DataListShell Props
| Prop | Type | Default | Description |
|---|---|---|---|
toolbar | ReactNode | — | Usually DataListToolbar. Ignored when renderToolbar is set. |
stats | ReactNode | — | KPIs between inquire filters and the list card. |
footer | ReactNode | — | Pagination and/or selection footer. |
filters | ReactNode | — | Sidebar FilterPanel. Below filtersBreakpoint it becomes a sheet. |
filtersWidth | number | 280 | Desktop rail width. |
filtersBreakpoint | "sm" | "md" | "lg" | "xl" | "lg" | Sheet below this breakpoint. |
filtersActiveCount | number | — | Badge on the Filters trigger. |
defaultFiltersOpen | boolean | true | Desktop rail open on first paint. |
DataListToolbar Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | headerTitle. |
tabs | ReactNode | — | DataListToolbarTabs — dataset pills, not page-channel tabs. |
actions | ReactNode | — | Primary cluster. Resource-index Create still belongs on Page extra. |
toolsStart | ReactNode | — | Leading tools (search). |
refresh | ReactNode | — | Usually DataListRefresh. Renders before tools, outside the refetch lock. |
tools | ReactNode | — | Column manager, density. |
filtersTrigger | ReactNode | — | Provided by the shell when a filter rail exists. |
onToggleFullscreen | () => void | — | Shows the fullscreen control only when passed. |
Status And Navigation
| Export | Role |
|---|---|
DataListPagination | Offset pager bound to the handle. Adapts to footer width, not the viewport. |
DataListLoadMore / DataListLoadMoreSentinel | Cursor / infinite recipes. |
DataListEmpty | No-data vs no-matches (reads active filters). Optional title, description, action. |
DataListErrorState | error + optional onRetry / isRetrying. |
DataListSearchInput | Debounced keyword → setSearch. Omit debounceMs / minChars to use handle.searchPolicy. Kit fallback is 300ms / 0 chars; createServerListAdapter publishes 500 / 2. Leaf props still override. |
DataListRefresh | Ghost-icon Reload. Calls handle.data.refetch(). While refetching, the same glyph spins in place (not a Spinner swap). Query List ships it on by default; headless hosts pass it as DataListToolbar refresh. |
DataListSelectionBar | variant="card" (default) or "embedded". Hidden while count === 0. |
DataListFooterToolbar | Selection-aware FooterToolbar; auto-teleports via PageFooter inside Page. |
DataListColumnFilter | Pass as Table renderColumnFilter. |
FilterPanel | Staged left-rail editor (sections, defaultOpen). Compose through DataListShell filters. |
DataListActiveFilters | Removable applied-filter chips. |
Column manager, density toggle, and Query Filter stay on Table / Query Filter / Query List.