Admin List Page
CRUD list archetype composing PageHeader, FilterBar, DataTable, BulkActionBar, and EmptyState into a single drop-in page.
Preview
Users
Manage everyone with access to this workspace.
| Jane Cooper | [email protected] | Admin | Active | |
| Wade Warren | [email protected] | Editor | Active | |
| Esther Howard | [email protected] | Editor | Invited | |
| Cameron Williamson | [email protected] | Viewer | Active | |
| Brooklyn Simmons | [email protected] | Admin | Suspended | |
| Leslie Alexander | [email protected] | Editor | Active | |
| Jenny Wilson | [email protected] | Viewer | Invited | |
| Guy Hawkins | [email protected] | Viewer | Active | |
| Robert Fox | [email protected] | Editor | Active | |
| Jacob Jones | [email protected] | Admin | Active |
Installation
npx visor add --block admin-list-pageThis copies files into your project:
blocks/admin-list-page/admin-list-page.tsx— the block componentblocks/admin-list-page/admin-list-page.module.css— the styles
The registry pulls in page-header, filter-bar, data-table, bulk-action-bar, and empty-state as dependencies.
Usage
'use client';
import * as React from 'react';
import { AdminListPage } from '@/blocks/admin-list-page/admin-list-page';
import { Button } from '@/components/ui/button/button';
import { Badge } from '@/components/ui/badge/badge';
import type { ColumnDef } from '@/components/ui/data-table/data-table';
interface User {
id: string;
name: string;
email: string;
role: string;
status: 'Active' | 'Invited' | 'Suspended';
}
export default function UsersPage() {
const [search, setSearch] = React.useState('');
const [users, setUsers] = React.useState<User[]>([]);
const columns = React.useMemo<ColumnDef<User, unknown>[]>(
() => [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'role', header: 'Role' },
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => <Badge>{row.original.status}</Badge>,
},
],
[]
);
const filtered = React.useMemo(() => {
const needle = search.trim().toLowerCase();
if (!needle) return users;
return users.filter(
(u) =>
u.name.toLowerCase().includes(needle) ||
u.email.toLowerCase().includes(needle)
);
}, [search, users]);
return (
<AdminListPage
eyebrow="Team"
title="Users"
description="Manage everyone with access to this workspace."
actions={<Button size="sm">New user</Button>}
searchValue={search}
onSearchChange={setSearch}
searchPlaceholder="Search users..."
columns={columns}
data={filtered}
getRowId={(row) => row.id}
enableRowSelection
bulkActions={
<>
<Button variant="outline" size="sm">Archive</Button>
<Button variant="destructive" size="sm">Delete</Button>
</>
}
/>
);
}Drop this inside an AdminShell to get the full chrome — sidebar, topbar, and list page in one composition.
Layout
The block is a vertical stack with a semantic structure:
- Header region — wraps the PageHeader (title, eyebrow, description, actions, breadcrumb) and the FilterBar (search input, filter slot, active filter chips, results count).
- Table region — wraps the DataTable and the optional BulkActionBar.
The root uses container queries so spacing scales with the column width handed to it by the surrounding AdminShell — no global breakpoints involved.
Controlled vs. uncontrolled
Every piece of orchestration state can be controlled or uncontrolled:
- Search — pass
searchValueandonSearchChangefor controlled, or omit both to hide the search input entirely. - Row selection — pass
rowSelectionandonRowSelectionChangefor controlled, or just setenableRowSelectionto let the block manage it internally. - Sorting and pagination — forwarded directly to the underlying DataTable, which supports both modes.
When uncontrolled, the block still drives the BulkActionBar — it counts the selected rows itself and wires onClear to reset the internal state.
Bulk actions
The BulkActionBar only renders when bulkActions is provided and the selected-row count is greater than zero. By default it is sticky at the bottom of the viewport; pass bulkActionBarInline to render it inline below the table instead.
Consumers own the action buttons — the block just provides the slot, the count, and the clear handler.
Empty state
The DataTable already renders a subtle "No results" EmptyState when data is empty. Pass emptyState to replace it entirely — for example, a CTA encouraging the user to create their first record.
CSS Hooks
The block exposes CSS custom property hooks so you can adjust its visual treatment without modifying the source files.
| Token | Default | Description |
|---|---|---|
--admin-list-page-table-bg | transparent | Background of the table section wrapper. |
--admin-list-page-table-radius | 0 | Border radius of the table section wrapper. |
--admin-list-page-table-header-radius | (DataTable default) | Top-corner radius of the sort-bar (column header row) inside the table. When unset, the DataTable's own --data-table-sort-bar-radius default (var(--radius-lg)) applies. Set to 0 for the borderless flush-header pattern — square header corners while the wrapper's card radius is unchanged. |
--admin-list-page-footer-bg | transparent | Background of the footer-status row. |
--admin-list-page-footer-radius | 0 | Border radius of the footer-status row. |
--admin-list-page-footer-border-top | 1px solid var(--border-subtle) | Top border of the footer-status row. |
--admin-list-page-footer-padding | var(--spacing-3) 0 | Padding of the footer-status row. |
Example — borderless admin card with flush (square-top) header:
/* In your page or theme styles */
[data-slot="admin-list-page"] {
--admin-list-page-table-bg: var(--surface-card);
--admin-list-page-table-radius: var(--radius-lg);
--admin-list-page-table-header-radius: 0;
}This gives the table section a rounded card shape while keeping the column-header row flush (no rounded top corners). Without --admin-list-page-table-header-radius: 0, the sort-bar would inherit the DataTable default and round its own top corners independently — visible when the header carries a filled background (e.g. --surface-subtle in the borderless-admin pattern).
Accessibility
- The top region is a semantic header element; the table region is a semantic section labelled by the page title when the title is a plain string.
- Search, sorting, pagination, and selection all inherit the accessibility guarantees of their underlying compounds.
- The BulkActionBar is a toolbar with Escape-to-clear behavior.
Props
See the AdminListPageProps interface in blocks/admin-list-page/admin-list-page.tsx for the full prop list. In summary:
- Header —
title(required),eyebrow,description,actions,breadcrumb,titleAs. - Filter bar —
searchValue,onSearchChange,searchPlaceholder,filters,activeFilters,onClearFilters,resultsCount,hideFilterBar. - Data table —
columns(required),data(required),getRowId,sorting,onSortingChange,pagination,onPaginationChange,pageSize,pageSizeOptions,rowSelection,onRowSelectionChange,enableRowSelection,loading,emptyState. - Bulk actions —
bulkActions,bulkActionBarInline,bulkActionLabel.
About Blocks
Blocks are complete UI patterns made up of multiple Visor components. Unlike individual components, blocks represent larger, composed sections of UI — such as admin shells, login forms, or dashboard panels.
Blocks are copy-and-own, just like components. Install them into your project and customize freely.
Admin Detail Drawer — Admin UI Density
Editorial-density composition of admin-detail-drawer demonstrating customHeader chrome row, hero section with KPI grid, line-variant Tabs with meta counts, and Kbd hints inside footerStatus. Built entirely from Visor primitives.
Admin Settings Page
Long scrollable settings archetype with labeled sections, optional sticky left-side nav with intersection-observer highlight, and either a global sticky save footer (default) or per-section save/revert rows.