VisorVisor
Blocks

Admin List Page

CRUD list archetype composing PageHeader, FilterBar, DataTable, BulkActionBar, and EmptyState into a single drop-in page.

Preview

Team

Users

Manage everyone with access to this workspace.

Jane Cooper[email protected]AdminActive
Wade Warren[email protected]EditorActive
Esther Howard[email protected]EditorInvited
Cameron Williamson[email protected]ViewerActive
Brooklyn Simmons[email protected]AdminSuspended
Leslie Alexander[email protected]EditorActive
Jenny Wilson[email protected]ViewerInvited
Guy Hawkins[email protected]ViewerActive
Robert Fox[email protected]EditorActive
Jacob Jones[email protected]AdminActive

Installation

npx visor add --block admin-list-page

This copies files into your project:

  • blocks/admin-list-page/admin-list-page.tsx — the block component
  • blocks/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:

  1. Header region — wraps the PageHeader (title, eyebrow, description, actions, breadcrumb) and the FilterBar (search input, filter slot, active filter chips, results count).
  2. 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 searchValue and onSearchChange for controlled, or omit both to hide the search input entirely.
  • Row selection — pass rowSelection and onRowSelectionChange for controlled, or just set enableRowSelection to 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.

TokenDefaultDescription
--admin-list-page-table-bgtransparentBackground of the table section wrapper.
--admin-list-page-table-radius0Border 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-bgtransparentBackground of the footer-status row.
--admin-list-page-footer-radius0Border radius of the footer-status row.
--admin-list-page-footer-border-top1px solid var(--border-subtle)Top border of the footer-status row.
--admin-list-page-footer-paddingvar(--spacing-3) 0Padding 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:

  • Headertitle (required), eyebrow, description, actions, breadcrumb, titleAs.
  • Filter barsearchValue, onSearchChange, searchPlaceholder, filters, activeFilters, onClearFilters, resultsCount, hideFilterBar.
  • Data tablecolumns (required), data (required), getRowId, sorting, onSortingChange, pagination, onPaginationChange, pageSize, pageSizeOptions, rowSelection, onRowSelectionChange, enableRowSelection, loading, emptyState.
  • Bulk actionsbulkActions, 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.