Skip to content
    Style: Wireframe

    Blocks · Data

    Data table

    A simple table: caption, header held while the rows scroll, sideways scroll in its own box on a narrow screen; person and link columns.

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

    Releases of Northwind, newest first

    Releases of Northwind, newest first
    VersionReleasedAuthorChangesHighlights
    2.4.04 Nov 2026Ada Lovelace18Dark mode for the settings pages
    2.3.221 Oct 2026Grace Hopper4Fixes the export for large accounts
    2.3.114 Oct 2026Alan Turing2Keyboard focus in the invoice list
    2.3.030 Sep 2026Ada Lovelace23Receipts by photo, bank feed filters
    2.2.02 Sep 2026Katherine Johnson15Recurring invoices
    2.1.419 Aug 2026Grace Hopper3Faster search

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    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

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    data-table.classes.ts
    /** 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
    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>
      );
    }