---
title: Data Table
description: Client-mode data table with sort, search, selection, density, and column manager
visibility: guest
draft: false
---

# Data Table

The single most-demanded surface in any app — orders, tickets, contacts, users,
records — is one thing: a sortable, searchable, bulk-selectable table. The
`data-table` wrapper handles the 80% case entirely client-side: hand it a `rows`
array and a `columns` definition and it sorts, searches, selects, toggles column
visibility, and switches density with no backend round-trip. For 10k+ rows you
drive the same UI from Livewire via the [server contract](#server-mode-large-datasets).

## Basic Usage

Define `columns` (each `{key, label}`) and pass `rows`. Headers are sortable by
default — click to sort, click again to reverse.

:::preview{title="A sortable data table"}
<x-wirekit::data-table
    caption="Customers"
    :columns="[
        ['key' => 'name', 'label' => 'Customer'],
        ['key' => 'plan', 'label' => 'Plan'],
        ['key' => 'mrr', 'label' => 'MRR', 'cellType' => 'number', 'align' => 'right'],
    ]"
    :rows="[
        ['id' => 1, 'name' => 'Acme Inc', 'plan' => 'Scale', 'mrr' => 1200],
        ['id' => 2, 'name' => 'Globex', 'plan' => 'Pro', 'mrr' => 540],
        ['id' => 3, 'name' => 'Initech', 'plan' => 'Starter', 'mrr' => 90],
        ['id' => 4, 'name' => 'Umbrella', 'plan' => 'Scale', 'mrr' => 2100],
    ]"
/>
:::

## Status Badges

A `cellType: 'badge'` column renders a tinted intent pill — common status words
(`paid`, `pending`, `failed`, …) map to success / warning / danger
automatically.

:::preview{title="Orders with status badges"}
<x-wirekit::data-table
    caption="Orders"
    :columns="[
        ['key' => 'ref', 'label' => 'Order'],
        ['key' => 'customer', 'label' => 'Customer'],
        ['key' => 'status', 'label' => 'Status', 'cellType' => 'badge'],
        ['key' => 'total', 'label' => 'Total', 'cellType' => 'number', 'align' => 'right'],
    ]"
    :rows="[
        ['id' => 1, 'ref' => '#1001', 'customer' => 'Acme Inc', 'status' => 'paid', 'total' => 1200],
        ['id' => 2, 'ref' => '#1002', 'customer' => 'Globex', 'status' => 'pending', 'total' => 540],
        ['id' => 3, 'ref' => '#1003', 'customer' => 'Initech', 'status' => 'failed', 'total' => 90],
    ]"
/>
:::

## Selection & Bulk Actions

Add `selectable` for per-row checkboxes plus a tri-state select-all. When rows
are selected, a bulk-action bar appears — fill it with the `bulkActions` slot.

:::preview{title="Selectable rows with a bulk-action bar"}
<x-wirekit::data-table
    selectable
    caption="Subscribers"
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'status', 'label' => 'Status', 'cellType' => 'badge'],
    ]"
    :rows="[
        ['id' => 1, 'name' => 'Dana Lee', 'email' => 'dana@example.com', 'status' => 'active'],
        ['id' => 2, 'name' => 'Sam Ray', 'email' => 'sam@example.com', 'status' => 'active'],
        ['id' => 3, 'name' => 'Kim Vo', 'email' => 'kim@example.com', 'status' => 'inactive'],
    ]"
>
    <x-slot:bulkActions>
        <x-wirekit::button intent="danger" surface="outline" size="sm">Delete</x-wirekit::button>
    </x-slot:bulkActions>
</x-wirekit::data-table>
:::

## Search, Density & Column Manager

`searchable` adds a client-side filter box, the density toggle switches row
padding, and `column-manager` lets the user show or hide columns.

:::preview{title="Toolbar with search, density, and column manager"}
<x-wirekit::data-table
    searchable
    column-manager
    search-placeholder="Search users…"
    caption="Users"
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'role', 'label' => 'Role'],
        ['key' => 'team', 'label' => 'Team'],
        ['key' => 'seats', 'label' => 'Seats', 'cellType' => 'number', 'align' => 'right'],
    ]"
    :rows="[
        ['id' => 1, 'name' => 'Dana Lee', 'role' => 'Admin', 'team' => 'Platform', 'seats' => 5],
        ['id' => 2, 'name' => 'Sam Ray', 'role' => 'Member', 'team' => 'Growth', 'seats' => 2],
        ['id' => 3, 'name' => 'Kim Vo', 'role' => 'Member', 'team' => 'Platform', 'seats' => 1],
    ]"
/>
:::

## Row Actions

The `rowActions` slot adds a trailing actions column. Compose a dropdown or
buttons; the slot renders once per row.

:::preview{title="A row-actions column"}
<x-wirekit::data-table
    caption="Invoices"
    :columns="[
        ['key' => 'ref', 'label' => 'Invoice'],
        ['key' => 'status', 'label' => 'Status', 'cellType' => 'badge'],
    ]"
    :rows="[
        ['id' => 1, 'ref' => 'INV-001', 'status' => 'paid'],
        ['id' => 2, 'ref' => 'INV-002', 'status' => 'overdue'],
    ]"
>
    <x-slot:rowActions>
        <x-wirekit::button surface="ghost" size="sm">View</x-wirekit::button>
    </x-slot:rowActions>
</x-wirekit::data-table>
:::

## Server Mode (Large Datasets)

For datasets too large to ship to the browser, add the `server` flag and drive
sort / search / selection from Livewire. The component stops sorting and
filtering locally and instead emits `sort-change`, `search-change`, and
`selection-change` events for you to handle:

```blade
{{-- 1. Server mode: the component renders the current page; you re-query --}}
<x-wirekit::data-table
    server
    selectable
    name="selectedIds"
    :columns="$columns"
    :rows="$this->rows()->items()"
    x-on:sort-change="$wire.set('sort', $event.detail)"
    x-on:search-change="$wire.set('search', $event.detail.value)"
/>

{{-- 2. Compose pagination below it --}}
{{ $this->rows()->links() }}
```

```php
// 3. A reusable trait carries the server-side state contract
trait WithDataTable
{
    public array $sort = ['key' => null, 'dir' => 'asc'];
    public string $search = '';
    public array $selectedIds = [];

    public function rows(): \Illuminate\Contracts\Pagination\LengthAwarePaginator
    {
        return Order::query()
            ->when($this->search, fn ($q) => $q->where('ref', 'like', "%{$this->search}%"))
            ->when($this->sort['key'], fn ($q) => $q->orderBy($this->sort['key'], $this->sort['dir']))
            ->paginate(25);
    }
}
```

The `selection-change` event (and the hidden input the `name` prop creates)
carry the selected id list, so `wire:model="selectedIds"` stays in sync for bulk
operations.

## Column Definition

| Key | Required | Description |
| --- | --- | --- |
| `key` | yes | The row field to read |
| `label` | yes | The header text |
| `sortable` | no | `false` disables sorting for the column |
| `align` | no | `left` (default) · `center` · `right` |
| `cellType` | no | `text` (default) · `number` (right tabular) · `badge` |

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rows` | array | `[]` | Row objects |
| `columns` | array | `[]` | Column definitions (see above) |
| `rowKey` | string | `'id'` | Unique id field (selection + morph keying) |
| `selectable` | bool | `false` | Per-row + select-all checkboxes |
| `searchable` | bool | `false` | Toolbar search box |
| `density` | string | `'comfortable'` | `comfortable` · `compact` |
| `columnManager` | bool | `false` | Show/hide-columns dropdown |
| `hidden` | array | `[]` | Initially-hidden column keys |
| `server` | bool | `false` | Server-driven — stop local sort/filter, emit events only |
| `searchPlaceholder` | string | `'Search…'` | Search box placeholder |
| `emptyText` | string | `'No results'` | Empty-state message |
| `caption` | string\|null | `null` | Accessible table caption / name |
| `name` | string\|null | `null` | Hidden-input name mirroring selected ids |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- The table sits in a labeled `role="region"` with `tabindex="0"` and a focus
  ring, so the horizontally-scrolling grid is keyboard-reachable (WCAG 2.1.1).
- Sortable headers are buttons carrying `aria-sort` (`ascending` / `descending`
  / `none`); the sort direction is shown with a caret glyph, not color.
- The select-all checkbox is tri-state (`indeterminate` when a subset is
  selected); the selection count is announced via an `aria-live` region.
- A `caption` becomes the table's accessible name.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Move through the toolbar, headers, checkboxes, and row actions |
| `Enter` / `Space` | Sort a column, toggle a checkbox, or trigger an action |

## Design Tokens

| Element | Token |
| --- | --- |
| Header text | `--color-wk-text-muted` |
| Row hover | `--color-wk-bg-subtle` |
| Selected row | `--color-wk-bg-muted` |
| Bulk-action bar | `--color-wk-bg-muted` |
| Checkbox accent | `--color-wk-accent` |
| Badge (success/warning/danger) | `--color-wk-success` / `--color-wk-warning` / `--color-wk-danger` |
| Focus ring | `--color-wk-ring` |

## Customization

Override the defaults via `config/wirekit.php`:

```php
'components' => [
    'data-table' => [
        'density' => 'compact',
        'selectable' => true,
        'searchable' => true,
    ],
],
```

## Further Reading

- [WAI-ARIA Authoring Practices — Sortable table](https://www.w3.org/WAI/ARIA/apg/patterns/table/)
- [Livewire — Pagination](https://livewire.laravel.com/docs/pagination)
