# Table

> A data table with custom cells.

## Examples

### Basic

```vue
<script setup lang="ts">
const columns = [
  { key: "name", header: "Member" },
  { key: "role", header: "Role" },
  { key: "commits", header: "Commits", numeric: true },
];

const rows = [
  {
    id: 1,
    name: "Ada Lovelace",
    email: "ada@example.com",
    avatar: "/avatars/ada.svg",
    role: "Maintainer",
    commits: 1243,
  },
  {
    id: 2,
    name: "Grace Hopper",
    email: "grace@example.com",
    avatar: "/avatars/grace.svg",
    role: "Reviewer",
    commits: 812,
  },
  { id: 3, name: "Alan Turing", email: "alan@example.com", role: "Contributor", commits: 97 },
];
</script>

<template>
  <UiTable :columns="columns" :rows="rows" row-key="id" caption="Contributors">
    <template #name-cell="{ row }">
      <span class="person">
        <UiAvatar :name="row.name" :src="row.avatar" size="small" />
        <span>
          {{ row.name }}
          <small>{{ row.email }}</small>
        </span>
      </span>
    </template>
    <template #role-cell="{ value }">
      <span class="badge" :data-tone="value === 'Maintainer' ? 'accent' : undefined">{{
        value
      }}</span>
    </template>
    <template #commits-cell="{ value }">{{ (value as number).toLocaleString("en-US") }}</template>
  </UiTable>
</template>
```

### Custom cells

Replace any cell, header or footer with the `<key>-cell`, `<key>-header` and `<key>-footer` slots.

```vue
<script setup lang="ts">
interface Invoice {
  id: string;
  customer: string;
  status: "paid" | "due" | "overdue";
  amount: number;
}

const columns = [
  { key: "id", header: "Invoice" },
  { key: "status", header: "Status" },
  { key: "amount", header: "Amount", numeric: true },
  { key: "actions", header: "", align: "right" as const, width: "64px" as const },
];

const rows: Invoice[] = [
  { id: "INV-001", customer: "Acme Inc.", status: "paid", amount: 1200 },
  { id: "INV-002", customer: "Globex", status: "due", amount: 860 },
  { id: "INV-003", customer: "Initech", status: "overdue", amount: 2400 },
];

const tones = { paid: "success", due: "warning", overdue: "danger" } as const;
const total = rows.reduce((sum, row) => sum + row.amount, 0);
const money = (value: number) => `$${value.toLocaleString("en-US")}`;

const toast = useToast();
const actions = [
  { label: "View", value: "view" },
  { label: "Download PDF", value: "download" },
  { label: "Void", value: "void" },
];
</script>

<template>
  <UiTable :columns="columns" :rows="rows" row-key="id">
    <template #id-cell="{ row }">
      <span class="stacked">
        {{ row.id }}
        <small>{{ row.customer }}</small>
      </span>
    </template>
    <template #status-cell="{ row }">
      <span class="badge" :data-tone="tones[row.status]">{{ row.status }}</span>
    </template>
    <template #amount-cell="{ row }">{{ money(row.amount) }}</template>
    <template #amount-footer>{{ money(total) }}</template>
    <template #actions-cell="{ row }">
      <UiMenu :items="actions" align="end" @select="(action) => toast.info(`${action} ${row.id}`)">
        <template #trigger="{ attrs }">
          <UiIconButton :label="`Actions for ${row.id}`" size="small" v-bind="attrs">
            <Icon name="more" :size="14" />
          </UiIconButton>
        </template>
      </UiMenu>
    </template>
  </UiTable>
</template>
```

### Sorting

Mark a column `sortable` and its header becomes a button that sets `v-model:sort`. The table doesn't reorder rows: sort them yourself, or on the server.

```vue
<script setup lang="ts">
import type { TableSort } from "@humanonlyweb/ui";

const columns = [
  { key: "name", header: "Package", sortable: true },
  { key: "size", header: "Size (kB)", numeric: true, sortable: true },
  { key: "downloads", header: "Weekly downloads", numeric: true, sortable: true },
];

const packages = [
  { name: "vue", size: 112, downloads: 7_100_000 },
  { name: "nuxt", size: 820, downloads: 1_050_000 },
  { name: "vite", size: 3_900, downloads: 31_000_000 },
  { name: "zod", size: 590, downloads: 26_000_000 },
];

const sort = ref<TableSort>({ key: "downloads", direction: "descending" });

// The table only reports the sort; ordering the rows (here, or on a server) is yours.
const rows = computed(() => {
  const { key, direction } = sort.value;
  const sign = direction === "ascending" ? 1 : -1;
  return packages.toSorted((a, b) => {
    const x = a[key as keyof typeof a];
    const y = b[key as keyof typeof b];
    return (
      (typeof x === "number" && typeof y === "number"
        ? x - y
        : String(x).localeCompare(String(y))) * sign
    );
  });
});
</script>

<template>
  <UiTable v-model:sort="sort" :columns="columns" :rows="rows" row-key="name" caption="Packages">
    <template #downloads-cell="{ value }">{{ Number(value).toLocaleString("en-US") }}</template>
  </UiTable>
</template>
```

### Empty

```vue
<script setup lang="ts">
const columns = [
  { key: "name", header: "Name" },
  { key: "email", header: "Email" },
];
</script>

<template>
  <UiTable :columns="columns" :rows="[]" caption="Members">
    <template #empty>
      <div class="empty">
        <p>No members yet</p>
        <UiButton size="small" variant="secondary">
          <template #leading><Icon name="plus" :size="14" /></template>
          Invite someone
        </UiButton>
      </div>
    </template>
  </UiTable>
</template>
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` (required) | `TableColumn<T>[]` | — | Column definitions: `key`, `header`, and optional `accessor`, `align`, `numeric`, `width`. |
| `rows` (required) | `T[]` | — | Row data. |
| `rowKey` | `((row: T, index: number) => PropertyKey) \| keyof T` | — | Stable key per row: a property name or a function. Defaults to the index. |
| `caption` | `string` | — | Visible caption. Also names the scroll region. |
| `sort` | `TableSort` | — | The sorted column and direction, set by `sortable` columns' headers. Rows aren't reordered for you. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:sort` | `[value: TableSort \| undefined]` | `v-model:sort` update. |

| Slot | Props | Description |
| --- | --- | --- |
| `empty` | — | Shown when there are no rows. |
| `sort-indicator` | `{ direction: "ascending" \| "descending" \| undefined; }` | Replaces a sortable header's direction arrow. |

Types:

```ts
export interface TableColumn<T> {
  key: string;
  header: string;
  accessor?: (row: T) => unknown;
  align?: TdHTMLAttributes["align"];
  numeric?: boolean;
  width?: `${number}px` | `${number}%`;
  /** Its header becomes a button that sets the table's `v-model:sort`. */
  sortable?: boolean;
}

/** A table's sort: which column, which way. Sorting the rows is up to you. */
export interface TableSort {
  key: string;
  direction: "ascending" | "descending";
}
```

## Styling

- Parts (`[data-part="…"]`): `table-scroll`, `table`, `table-caption`, `table-header`, `table-row`, `table-cell`, `table-sort`, `table-sort-indicator`, `table-empty`, `table-footer`
- State attributes: `data-table-overflow`, `data-table-align`, `data-table-numeric`, `data-table-sort`

## Accessibility

- A table wider than its container scrolls in a region named by `caption`, focusable so the keyboard can scroll it.
