# Data table (themes.dev UI block)

A simple table: caption, header held while the rows scroll, sideways scroll in its own box on a narrow screen; person and link columns. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

5.4% of the 2,073 sites in The UI Index have a table (112 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/ui/r/data-table.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/ui/r/astro/data-table.json`
- HTML: https://www.themes.dev/ui/r/html/data-table.html (with https://www.themes.dev/ui/r/html/caseroom.css)

Page: https://www.themes.dev/ui/blocks/data-table/ · Rules for agents: https://www.themes.dev/ui/AGENT.md

## Code

### data-table.astro

```astro
---
import Avatar from '@/components/ui/avatar.astro';
import Link from '@/components/ui/link.astro';
import { cn } from '@/lib/utils';
import { dataTable as c, type DataTableContent } from './data-table.classes';

type Props = DataTableContent & { class?: string };
const { caption, columns, rows, id, class: className } = Astro.props;
---

<div class={cn(c.root, className)}>
  <p id={`${id}-caption`} class={c.caption}>{caption}</p>
  <div class={c.wrap(rows.length)} tabindex="0" role="region" aria-labelledby={`${id}-caption`}>
    <table class={c.table}>
      <caption class="sr-only">{caption}</caption>
      <thead>
        <tr>
          {columns.map((col) => <th scope="col" class={cn(c.head(rows.length), col.align === 'end' && 'text-right')}>{col.label}</th>)}
        </tr>
      </thead>
      <tbody>
        {
          rows.map((r) => (
            <tr class={c.row}>
              {columns.map((col, i) => {
                const val = r.cells[col.key];
                const kind = col.kind ?? 'text';
                return (
                  <td class={cn(c.cell, col.align === 'end' && c.end)}>
                    {kind === 'person' ? (
                      <span class={c.person}>
                        <span aria-hidden="true"><Avatar name={String(val)} size="sm" /></span>
                        {val}
                      </span>
                    ) : kind === 'link' && r.href ? (
                      <Link href={r.href} class={c.link}>{val}</Link>
                    ) : i === 0 && !col.align ? (
                      <span class={c.first}>{val}</span>
                    ) : (
                      val
                    )}
                  </td>
                );
              })}
            </tr>
          ))
        }
      </tbody>
    </table>
  </div>
</div>
```

### data-table.classes.ts

```tsx
/** A simple data table: a caption above, a real <table> with scoped header cells in a scroll box of its own
 *  (focusable, named by the caption) that scrolls sideways on a narrow screen and keeps the header in view when the
 *  rows are many. Rows keep their hairlines: lines are for data. A person column draws a monogram and the name. */
export type DataTableKind = 'text' | 'person' | 'link';
export type DataTableColumn = {
  /** The key of the cell in each row. */
  key: string;
  /** The header text. At most 24 characters. */
  label: string;
  kind?: DataTableKind;
  /** Right-aligns the column in tabular figures. */
  align?: 'start' | 'end';
};
export type DataTableRow = {
  cells: Record<string, string | number>;
  /** Where a link column goes. */
  href?: string;
};
export type DataTableContent = {
  /** Names the table and the scroll box. At most 80 characters. */
  caption: string;
  columns: DataTableColumn[];
  rows: DataTableRow[];
  /** Unique on the page; ids the caption. */
  id: string;
};

/** More rows than this and the box stops growing and scrolls, header held. */
export const DATA_TABLE_MAX_ROWS = 8;

export const dataTable = {
  root: '@container flex w-full min-w-0 flex-col gap-4 bg-background px-6 py-14 text-foreground',
  caption: 'text-base font-semibold tracking-[-0.01em]',
  wrap: (rowCount: number) =>
    `relative w-full overflow-auto rounded-lg border bg-background [background:linear-gradient(to_right,var(--background)_50%,transparent)_left/2rem_100%_no-repeat_local,linear-gradient(to_left,var(--background)_50%,transparent)_right/2rem_100%_no-repeat_local,radial-gradient(farthest-side_at_0_50%,color-mix(in_oklab,var(--foreground)_18%,transparent),transparent)_left/0.75rem_100%_no-repeat_scroll,radial-gradient(farthest-side_at_100%_50%,color-mix(in_oklab,var(--foreground)_18%,transparent),transparent)_right/0.75rem_100%_no-repeat_scroll,var(--background)] ${rowCount > DATA_TABLE_MAX_ROWS ? 'max-h-[28rem]' : ''}`,
  table: 'w-full min-w-max border-separate border-spacing-0 text-sm',
  head: (rowCount: number) =>
    `h-10 border-b px-4 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground shadow-[inset_0_-1px_0_var(--border)] max-sm:px-3 ${rowCount > DATA_TABLE_MAX_ROWS ? 'sticky top-0 z-10 bg-background' : ''}`,
  cell: 'border-b px-4 py-3 align-middle whitespace-nowrap max-sm:px-3',
  row: 'transition-colors hover:bg-muted/60 motion-reduce:transition-none [&:last-child>td]:border-b-0',
  end: 'text-right tabular-nums',
  first: 'font-medium',
  person: 'inline-flex items-center gap-2',
  link: 'font-medium',
};
```

### data-table.tsx

```tsx
import { Avatar } from '@/components/ui/avatar';
import { Link } from '@/components/ui/link';
import { cn } from '@/lib/utils';
import { dataTable as c, type DataTableContent } from './data-table.classes';

export type DataTableProps = DataTableContent & { className?: string };

/** A simple data table: caption, held header, sideways scroll in its own box. */
export function DataTable({ caption, columns, rows, id, className }: DataTableProps) {
  return (
    <div className={cn(c.root, className)}>
      <p id={`${id}-caption`} className={c.caption}>{caption}</p>
      <div className={c.wrap(rows.length)} tabIndex={0} role="region" aria-labelledby={`${id}-caption`}>
        <table className={c.table}>
          <caption className="sr-only">{caption}</caption>
          <thead>
            <tr>
              {columns.map((col) => <th key={col.key} scope="col" className={cn(c.head(rows.length), col.align === 'end' && 'text-right')}>{col.label}</th>)}
            </tr>
          </thead>
          <tbody>
            {rows.map((r, ri) => (
              <tr key={ri} className={c.row}>
                {columns.map((col, i) => {
                  const val = r.cells[col.key];
                  const kind = col.kind ?? 'text';
                  return (
                    <td key={col.key} className={cn(c.cell, col.align === 'end' && c.end)}>
                      {kind === 'person' ? (
                        <span className={c.person}>
                          <span aria-hidden="true"><Avatar name={String(val)} size="sm" /></span>
                          {val}
                        </span>
                      ) : kind === 'link' && r.href ? (
                        <Link href={r.href} className={c.link}>{val}</Link>
                      ) : i === 0 && !col.align ? (
                        <span className={c.first}>{val}</span>
                      ) : (
                        val
                      )}
                    </td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}
```
