# Customers Table

> Accounts ranked by MRR with plan badges, churn-risk indicators and owner avatars.

A shadcn/ui block from the [@spectrumui](https://ui.spectrumhq.in) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/spectrumui/customers-table).

- Page, with a live preview: https://shadcn.exchange/r/spectrumui/customers-table
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/spectrumui/customers-table-light-721a15acdb93b30c.png
- Registry item: https://ui.spectrumhq.in/r/customers-table.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.spectrumhq.in/r/customers-table.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@spectrumui/data-table`

## Files

### `components/spectrumui/blocks/tables/customers-table.tsx`

```tsx
'use client';

import * as React from 'react';
import { DataTable, type DataTableColumn } from '@/components/spectrumui/data-table';

type Risk = 'low' | 'medium' | 'high';

interface Customer {
  id: string;
  company: string;
  owner: string;
  avatar: string;
  plan: 'Enterprise' | 'Scale' | 'Team' | 'Starter';
  mrr: number;
  risk: Risk;
  since: string;
}

const CUSTOMERS: Customer[] = [
  {
    id: 'c1',
    company: 'Driftlab',
    owner: 'Freya Lindgren',
    avatar: '/avatars/people/01.jpg',
    plan: 'Enterprise',
    mrr: 4200,
    risk: 'low',
    since: 'Jan 2024',
  },
  {
    id: 'c2',
    company: 'Keelworks',
    owner: 'Mateo Alvarez',
    avatar: '/avatars/people/03.jpg',
    plan: 'Scale',
    mrr: 1840,
    risk: 'low',
    since: 'Mar 2024',
  },
  {
    id: 'c3',
    company: 'Lumenshift',
    owner: 'Aiko Watanabe',
    avatar: '/avatars/people/06.jpg',
    plan: 'Scale',
    mrr: 1420,
    risk: 'medium',
    since: 'Nov 2024',
  },
  {
    id: 'c4',
    company: 'Fernwerk',
    owner: 'Jonas Keller',
    avatar: '/avatars/people/09.jpg',
    plan: 'Team',
    mrr: 640,
    risk: 'high',
    since: 'Feb 2025',
  },
  {
    id: 'c5',
    company: 'Copperline',
    owner: 'Zara Osei',
    avatar: '/avatars/people/12.jpg',
    plan: 'Team',
    mrr: 520,
    risk: 'low',
    since: 'Jun 2025',
  },
  {
    id: 'c6',
    company: 'Havenpoint',
    owner: 'Nils Bakker',
    avatar: '/avatars/people/13.jpg',
    plan: 'Starter',
    mrr: 96,
    risk: 'medium',
    since: 'Apr 2026',
  },
  {
    id: 'c7',
    company: 'Sable & Co',
    owner: 'Leila Haddad',
    avatar: '/avatars/people/05.jpg',
    plan: 'Enterprise',
    mrr: 3600,
    risk: 'low',
    since: 'Sep 2023',
  },
];

const RISK_STYLE: Record<Risk, string> = {
  low: 'text-emerald-700 dark:text-emerald-300',
  medium: 'text-amber-700 dark:text-amber-300',
  high: 'text-rose-700 dark:text-rose-300',
};

const RISK_RANK: Record<Risk, number> = { low: 0, medium: 1, high: 2 };

const money = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  maximumFractionDigits: 0,
});

const columns: DataTableColumn<Customer>[] = [
  {
    id: 'company',
    header: 'Customer',
    sortable: true,
    value: (row) => row.company,
    cell: (row) => (
      <div className="flex items-center gap-2.5">
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={row.avatar}
          alt=""
          loading="lazy"
          width={28}
          height={28}
          className="size-7 shrink-0 rounded-full object-cover outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10"
        />
        <span className="min-w-0">
          <span className="block truncate">{row.company}</span>
          <span className="block truncate text-xs font-normal text-neutral-500 dark:text-neutral-400">
            {row.owner}
          </span>
        </span>
      </div>
    ),
  },
  {
    id: 'plan',
    header: 'Plan',
    sortable: true,
    value: (row) => row.plan,
    cell: (row) => (
      <span className="inline-flex rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-700 ring-1 ring-inset ring-neutral-500/15 dark:bg-neutral-100/10 dark:text-neutral-300 dark:ring-white/15">
        {row.plan}
      </span>
    ),
  },
  {
    id: 'mrr',
    header: 'MRR',
    sortable: true,
    numeric: true,
    value: (row) => row.mrr,
    formatTotal: (sum) => money.format(sum),
    cell: (row) => (
      <span className="font-medium text-neutral-900 dark:text-neutral-100">
        {money.format(row.mrr)}
      </span>
    ),
  },
  {
    id: 'risk',
    header: 'Churn risk',
    sortable: true,
    hideBelow: 'sm',
    value: (row) => RISK_RANK[row.risk],
    cell: (row) => (
      <span
        className={`inline-flex items-center gap-1.5 font-medium capitalize ${RISK_STYLE[row.risk]}`}
      >
        <span aria-hidden="true" className="size-1.5 rounded-full bg-current" />
        {row.risk}
      </span>
    ),
  },
  { id: 'since', header: 'Since', hideBelow: 'lg', value: (row) => row.since },
];

export function CustomersTable({
  variant = 'Comfortable',
}: {
  variant?: 'Comfortable' | 'Compact';
}) {
  return (
    <div className="mx-auto w-full max-w-4xl px-4 py-10 sm:px-6">
      <DataTable
        data={CUSTOMERS}
        columns={columns}
        rowId={(row) => row.id}
        rowLabel={(row) => row.company}
        caption="Customers ranked by monthly recurring revenue, with plan and churn risk."
        density={variant === 'Compact' ? 'compact' : 'default'}
        title="Customers"
        quickFilter={{ columnId: 'plan', label: 'Filter by plan', getValue: (row) => row.plan }}
        totals={['mrr']}
        defaultSort={{ columnId: 'mrr', direction: 'desc' }}
      />
    </div>
  );
}
```
