# Support Tickets Table

> A support queue with priority icons, SLA countdowns, assignee avatars and status pills.

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

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

## Install

```bash
npx shadcn@latest add https://ui.spectrumhq.in/r/tickets-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/tickets-table.tsx`

```tsx
'use client';

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

type IconProps = React.SVGProps<SVGSVGElement>;

function IconDanger(props: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
      <path
        transform="translate(2, 3)"
        d="M8.628,0.353 C9.988,-0.401 11.717,0.094 12.477,1.442 L12.477,1.442 L19.746,14.057 C19.906,14.434 19.976,14.74 19.996,15.058 C20.036,15.801 19.776,16.524 19.266,17.079 C18.756,17.633 18.066,17.96 17.316,18 L17.316,18 L2.679,18 C2.369,17.981 2.059,17.911 1.769,17.802 C0.319,17.217 -0.381,15.572 0.209,14.146 L0.209,14.146 L7.528,1.433 C7.778,0.986 8.158,0.601 8.628,0.353 Z M9.998,12.273 C9.518,12.273 9.118,12.669 9.118,13.146 C9.118,13.62 9.518,14.018 9.998,14.018 C10.478,14.018 10.868,13.62 10.868,13.135 C10.868,12.66 10.478,12.273 9.998,12.273 Z M9.998,6.09 C9.518,6.09 9.118,6.476 9.118,6.952 L9.118,6.952 L9.118,9.756 C9.118,10.231 9.518,10.629 9.998,10.629 C10.478,10.629 10.868,10.231 10.868,9.756 L10.868,9.756 L10.868,6.952 C10.868,6.476 10.478,6.09 9.998,6.09 Z"
      />
    </svg>
  );
}

function IconTime(props: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
      <path
        transform="translate(2, 2)"
        d="M10,0 C15.53,0 20,4.48 20,10 C20,15.53 15.53,20 10,20 C4.48,20 0,15.53 0,10 C0,4.48 4.48,0 10,0 Z M9.65,4.93 C9.24,4.93 8.9,5.26 8.9,5.68 L8.9,5.68 L8.9,10.73 C8.9,10.99 9.04,11.23 9.27,11.37 L9.27,11.37 L13.19,13.71 C13.31,13.78 13.44,13.82 13.58,13.82 C13.83,13.82 14.08,13.69 14.22,13.45 C14.43,13.1 14.32,12.64 13.96,12.42 L13.96,12.42 L10.4,10.3 L10.4,5.68 C10.4,5.26 10.06,4.93 9.65,4.93 Z"
      />
    </svg>
  );
}

type Priority = 'urgent' | 'high' | 'normal';
type Status = 'open' | 'pending' | 'solved';

interface Ticket {
  id: string;
  subject: string;
  requester: string;
  requesterAvatar: string;
  priority: Priority;
  sla: string;
  breaching: boolean;
  assignee: string;
  assigneeAvatar: string;
  status: Status;
}

const TICKETS: Ticket[] = [
  {
    id: '#3128',
    subject: 'SSO login loops back to the sign-in page',
    requester: 'Jonas Keller',
    requesterAvatar: '/avatars/people/09.jpg',
    priority: 'urgent',
    sla: '38m left',
    breaching: true,
    assignee: 'Freya',
    assigneeAvatar: '/avatars/people/01.jpg',
    status: 'open',
  },
  {
    id: '#3127',
    subject: 'Webhooks delayed by ~10 minutes',
    requester: 'Zara Osei',
    requesterAvatar: '/avatars/people/12.jpg',
    priority: 'high',
    sla: '3h left',
    breaching: false,
    assignee: 'Mateo',
    assigneeAvatar: '/avatars/people/03.jpg',
    status: 'open',
  },
  {
    id: '#3126',
    subject: 'Cannot download August invoice PDF',
    requester: 'Nils Bakker',
    requesterAvatar: '/avatars/people/13.jpg',
    priority: 'normal',
    sla: '8h left',
    breaching: false,
    assignee: 'Aiko',
    assigneeAvatar: '/avatars/people/06.jpg',
    status: 'pending',
  },
  {
    id: '#3125',
    subject: 'Add seats mid-cycle proration question',
    requester: 'Leila Haddad',
    requesterAvatar: '/avatars/people/05.jpg',
    priority: 'normal',
    sla: '1d left',
    breaching: false,
    assignee: 'Freya',
    assigneeAvatar: '/avatars/people/01.jpg',
    status: 'pending',
  },
  {
    id: '#3124',
    subject: 'API returns 429 on burst traffic',
    requester: 'Tomas Vega',
    requesterAvatar: '/avatars/people/08.jpg',
    priority: 'high',
    sla: 'Met',
    breaching: false,
    assignee: 'Mateo',
    assigneeAvatar: '/avatars/people/03.jpg',
    status: 'solved',
  },
  {
    id: '#3123',
    subject: 'Dark mode logo renders on light emails',
    requester: 'Freya Lindgren',
    requesterAvatar: '/avatars/people/01.jpg',
    priority: 'normal',
    sla: 'Met',
    breaching: false,
    assignee: 'Aiko',
    assigneeAvatar: '/avatars/people/06.jpg',
    status: 'solved',
  },
];

const PRIORITY_RANK: Record<Priority, number> = { urgent: 0, high: 1, normal: 2 };

const PRIORITY_STYLE: Record<Priority, string> = {
  urgent: 'text-rose-700 dark:text-rose-300',
  high: 'text-amber-700 dark:text-amber-300',
  normal: 'text-neutral-500 dark:text-neutral-400',
};

const STATUS_STYLE: Record<Status, string> = {
  open: 'bg-amber-50 text-amber-800 ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-300/25',
  pending:
    'bg-neutral-100 text-neutral-600 ring-neutral-500/20 dark:bg-neutral-100/10 dark:text-neutral-300 dark:ring-white/15',
  solved:
    'bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-300/25',
};

const columns: DataTableColumn<Ticket>[] = [
  {
    id: 'subject',
    header: 'Ticket',
    sortable: true,
    value: (row) => row.subject,
    cell: (row) => (
      <span className="block min-w-0 max-w-72">
        <span className="block truncate">{row.subject}</span>
        <span className="block text-xs font-normal text-neutral-500 dark:text-neutral-400">
          {row.id} · {row.requester}
        </span>
      </span>
    ),
  },
  {
    id: 'priority',
    header: 'Priority',
    sortable: true,
    value: (row) => PRIORITY_RANK[row.priority],
    cell: (row) => (
      <span
        className={`inline-flex items-center gap-1.5 font-medium capitalize ${PRIORITY_STYLE[row.priority]}`}
      >
        {row.priority === 'normal' ? (
          <span aria-hidden="true" className="size-1.5 rounded-full bg-current" />
        ) : (
          <IconDanger className="size-3.5" />
        )}
        {row.priority}
      </span>
    ),
  },
  {
    id: 'sla',
    header: 'SLA',
    hideBelow: 'sm',
    value: (row) => row.sla,
    cell: (row) => (
      <span
        className={`inline-flex items-center gap-1.5 tabular-nums ${
          row.breaching
            ? 'font-medium text-rose-700 dark:text-rose-300'
            : 'text-neutral-500 dark:text-neutral-400'
        }`}
      >
        <IconTime className="size-3.5" />
        {row.sla}
      </span>
    ),
  },
  {
    id: 'assignee',
    header: 'Assignee',
    sortable: true,
    hideBelow: 'md',
    value: (row) => row.assignee,
    cell: (row) => (
      <span className="flex items-center gap-2 font-normal">
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={row.assigneeAvatar}
          alt=""
          loading="lazy"
          width={20}
          height={20}
          className="size-5 shrink-0 rounded-full object-cover outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10"
        />
        {row.assignee}
      </span>
    ),
  },
  {
    id: 'status',
    header: 'Status',
    sortable: true,
    value: (row) => row.status,
    cell: (row) => (
      <span
        className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium capitalize ring-1 ring-inset ${STATUS_STYLE[row.status]}`}
      >
        <span aria-hidden="true" className="size-1.5 rounded-full bg-current" />
        {row.status}
      </span>
    ),
  },
];

export function TicketsTable({ variant = 'Comfortable' }: { variant?: 'Comfortable' | 'Striped' }) {
  return (
    <div className="mx-auto w-full max-w-4xl px-4 py-10 sm:px-6">
      <DataTable
        data={TICKETS}
        columns={columns}
        rowId={(row) => row.id}
        rowLabel={(row) => `ticket ${row.id}`}
        caption="Open support tickets with priority, SLA countdown and assignee."
        variant={variant === 'Striped' ? 'striped' : 'default'}
        title="Tickets"
        quickFilter={{ columnId: 'status', label: 'Filter by status' }}
        defaultSort={{ columnId: 'priority', direction: 'asc' }}
      />
    </div>
  );
}
```
