Design System

Contact List Pattern

Use this pattern for any entity list page with DataTable, search, sorting, pagination, and StatusBadge.

Live Demo

NameEmailCompanyStatus
Sarah ChenCTOsarah.chen@acme.comAcme CorpCustomer
Marcus JohnsonFoundermarcus.j@techstart.ioTechStartLead
Elena RodriguezPartnerelena@globalventures.comGlobal VenturesActive
David KimVP Engineeringdkim@enterprise.coEnterprise CoCustomer
Aisha PatelCEOaisha.patel@innovate.devInnovate DevInactive
Showing 1 to 5 of 5 results
Rows per page
Page 1 of 1

Source

tsx
'use client';

import { createColumnHelper } from '@tanstack/react-table';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import { Button, StatusBadge } from '@stackmates/ui-interactive/atoms';
import { DataTable } from '@stackmates/ui-interactive/organisms';
import { CONTACTS, CONTACT_STATUS_BADGE_MAP, STATUS_LABELS,
  STATUS_OPTIONS, type Contact, type ContactStatus } from '../_data';

const columnHelper = createColumnHelper<Contact>();

const columns = [
  columnHelper.accessor(row => `${row.firstName} ${row.lastName}`, {
    id: 'name', header: 'Name',
    cell: info => <Link href={`/crm/contacts/${info.row.original.id}`}>
      <span className="font-medium">{info.getValue()}</span>
    </Link>,
  }),
  columnHelper.accessor('email', { header: 'Email' }),
  columnHelper.accessor('company', { header: 'Company' }),
  columnHelper.accessor('status', {
    header: 'Status',
    cell: info => (
      <StatusBadge status={CONTACT_STATUS_BADGE_MAP[info.getValue()]}
        showIcon={false} size="sm">
        {STATUS_LABELS[info.getValue()]}
      </StatusBadge>
    ),
  }),
];

export default function ContactListPage() {
  const [globalFilter, setGlobalFilter] = useState('');
  const [statusFilter, setStatusFilter] = useState<ContactStatus | 'all'>('all');

  const filtered = useMemo(() => {
    if (statusFilter === 'all') return CONTACTS;
    return CONTACTS.filter(c => c.status === statusFilter);
  }, [statusFilter]);

  return (
    <div>
      <select value={statusFilter}
        onChange={e => setStatusFilter(e.target.value as ContactStatus | 'all')}>
        <option value="all">All Statuses</option>
        {STATUS_OPTIONS.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
      </select>
      <Link href="/crm/contacts/new"><Button>Add Contact</Button></Link>

      <DataTable
        data={filtered} columns={columns} getRowId={row => row.id}
        enableSorting enablePagination
        globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter}
        searchPlaceholder="Search contacts..."
        pageSizeOptions={[10, 25, 50]}
        emptyMessage="No contacts found." hoverable
      />
    </div>
  );
}

Copy these 2 files

crud/_data.tsTypes, mock data, CONTACT_STATUS_BADGE_MAP, search helpers
crud/contact-list/page.tsxList page with DataTable, globalFilter search, status filter

Customization

  1. Replace Contact with your entity name and update column definitions
  2. Map your status enum to StatusBadge variants via a badge map
  3. Add enableRowSelection for bulk operations
  4. Use CRUDTable instead of DataTable if you need inline row actions
  5. Replace the status <select> with SearchFilterBar for complex filtering

Anti-patterns

Wrong

Use raw <table> with manual thead/tbody

Right

Use DataTable from @stackmates/ui-interactive/organisms with enableSorting + enablePagination

Wrong

Hardcode inline status color classes (STATUS_COLORS map)

Right

Use StatusBadge from @stackmates/ui-interactive/atoms with a status mapping

Wrong

Build search from scratch with raw <input type="search">

Right

Use DataTable globalFilter prop for built-in search, or SearchFilterBar for complex filters