---
title: Table
description: Data table with sorting and styling options
visibility: guest
draft: false
---

# Table

A flexible table component with sub-components for head, body, foot, row, th, and td. Supports striped rows, hoverable rows, compact density, sortable columns, and horizontal scroll on narrow screens.

> **Wrap your rows in the `table.*` sub-components — not in raw `<thead>` / `<tbody>` / `<tr>` / `<td>` HTML.** `<x-wirekit::table>` only styles the outer `<table>` element. Padding, row dividers, stripe + hover wiring, and sticky-header support live on the sub-components. Dropping plain HTML inside compiles cleanly and renders without errors — but produces a visually-broken table with flush-to-border content and no row separators. The component prints a `console.warn` in debug mode when it detects plain `<thead>` / `<tbody>` / `<tr>` / `<th>` / `<td>` descendants; production stays silent. As a safety net, raw `<td>` / `<th>` cells inside `<x-wirekit::table>` now receive the same readable cell padding the `table.td` sub-component uses — so a raw table is no longer cramped flush-to-border even before you migrate it to the sub-components.

## Basic Usage

:::preview{title="Simple table"}
<x-wirekit::table>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Name</x-wirekit::table.th>
            <x-wirekit::table.th>Role</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Balance</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Jane Doe</x-wirekit::table.td>
            <x-wirekit::table.td>Administrator</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€1,250.00</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>John Smith</x-wirekit::table.td>
            <x-wirekit::table.td>Editor</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€840.00</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

## Variants

Variant flags on the parent `<x-wirekit::table>` cascade to all rows via data attributes — no need to configure each sub-component.

### Striped Rows

Alternating row tint, useful for dense data.

:::preview{title="Striped + hoverable + compact"}
<x-wirekit::table striped hoverable compact>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Product</x-wirekit::table.th>
            <x-wirekit::table.th>SKU</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Stock</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Keyboard</x-wirekit::table.td>
            <x-wirekit::table.td>KB-001</x-wirekit::table.td>
            <x-wirekit::table.td align="right">42</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Mouse</x-wirekit::table.td>
            <x-wirekit::table.td>MS-014</x-wirekit::table.td>
            <x-wirekit::table.td align="right">128</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Monitor</x-wirekit::table.td>
            <x-wirekit::table.td>MN-220</x-wirekit::table.td>
            <x-wirekit::table.td align="right">7</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

Variant flags compose freely — any combination of `striped`, `hoverable`, and `compact` works. Example:

```blade
<x-wirekit::table striped hoverable compact>...</x-wirekit::table>
```

## Sortable Columns

WireKit tables support two sorting modes: **Livewire** (server-side) and **Alpine** (client-side). Both render the same clickable headers with direction indicators and proper ARIA attributes.

Add `sortable` to `<x-wirekit::table.th>` to render a clickable header with a direction indicator. The header wires up automatically: with `alpine-sort` on the table, clicks cycle direction (unsorted → ascending → descending → unsorted) and reorder rows client-side. With Livewire (next section), bind `sort-direction` and `wire:click` to drive sorting server-side.

:::preview{title="Sortable headers (Alpine — interactive)"}
<x-wirekit::table alpine-sort hoverable>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th sortable column="name">Name</x-wirekit::table.th>
            <x-wirekit::table.th sortable column="created">Created</x-wirekit::table.th>
            <x-wirekit::table.th sortable column="status">Status</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Alpha</x-wirekit::table.td>
            <x-wirekit::table.td>2025-01-15</x-wirekit::table.td>
            <x-wirekit::table.td>Active</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Beta</x-wirekit::table.td>
            <x-wirekit::table.td>2025-03-08</x-wirekit::table.td>
            <x-wirekit::table.td>Draft</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Gamma</x-wirekit::table.td>
            <x-wirekit::table.td>2025-02-22</x-wirekit::table.td>
            <x-wirekit::table.td>Active</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

Click any column header above to cycle through ascending → descending → unsorted. The component sets `aria-sort="ascending|descending|none"` automatically per the WAI-ARIA contract for assistive technologies.

### Livewire Integration

In a Livewire component, bind `sort-direction` dynamically and pass the sort method call to `sort-action`. Unlike putting `wire:click` on the header cell itself, `sort-action` wraps the label in a real `<button>`, so the sort is operable by keyboard (WCAG 2.1.1) and gets a visible focus ring:

::: info
The `<button>` fills the header cell, so the whole cell is the click target and the
padding is not a dead zone. That matters on touch: a button sized to its label alone
is only as tall as one line of text, which is under the 24×24 minimum of
[WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html).
The cell keeps its original height — the padding moves onto the button rather than
being added to it — and the focus ring is drawn inside the cell so the table's scroll
container cannot clip it on the header row.
:::

```blade
<x-wirekit::table hoverable>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            @foreach(['name' => 'Name', 'email' => 'Email'] as $field => $label)
                <x-wirekit::table.th
                    sortable
                    :sort-direction="$sortField === $field ? $sortDir : null"
                    :sort-action="\"sortBy('{$field}')\""
                >
                    {{ $label }}
                </x-wirekit::table.th>
            @endforeach
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        @foreach($users as $user)
            <x-wirekit::table.row>
                <x-wirekit::table.td>{{ $user->name }}</x-wirekit::table.td>
                <x-wirekit::table.td>{{ $user->email }}</x-wirekit::table.td>
            </x-wirekit::table.row>
        @endforeach
    </x-wirekit::table.body>
</x-wirekit::table>
```

### Client-Side Sorting (Alpine)

For static or small tables that don't need a server round-trip, use the `alpine-sort` prop on the table and `column` on each sortable `<th>`. This enables fully client-side sorting powered by Alpine.js — no Livewire required.

```blade
<x-wirekit::table alpine-sort hoverable>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th sortable column="name">Name</x-wirekit::table.th>
            <x-wirekit::table.th sortable column="role">Role</x-wirekit::table.th>
            <x-wirekit::table.th sortable column="balance" align="right">Balance</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Jane Doe</x-wirekit::table.td>
            <x-wirekit::table.td>Administrator</x-wirekit::table.td>
            <x-wirekit::table.td align="right" data-wk-sort-value="1250">€1,250.00</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>John Smith</x-wirekit::table.td>
            <x-wirekit::table.td>Editor</x-wirekit::table.td>
            <x-wirekit::table.td align="right" data-wk-sort-value="840">€840.00</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Alice Lee</x-wirekit::table.td>
            <x-wirekit::table.td>Viewer</x-wirekit::table.td>
            <x-wirekit::table.td align="right" data-wk-sort-value="2100">€2,100.00</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
```

**How it works:**

- Clicking a column header cycles through: **unsorted → ascending → descending → unsorted**
- The sort component reads cell values from `data-wk-sort-value` (if present) or falls back to the cell's text content
- Numeric values are compared numerically; strings use locale-aware comparison
- The original row order is preserved and restored when sorting is cleared
- `aria-sort` is updated dynamically via Alpine bindings

**When to use `data-wk-sort-value`:** Use it when the display value differs from the sort value — for example, formatted currencies (`€1,250.00` displays but `1250` sorts), dates, or values with units. If the text content is already sortable (plain text, plain numbers), you can omit it.

## Caption

Add a semantic `<caption>` to describe the table content for screen readers:

:::preview{title="Table with caption"}
<x-wirekit::table>
    <x-wirekit::table.caption>A list of active team members and their roles.</x-wirekit::table.caption>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Name</x-wirekit::table.th>
            <x-wirekit::table.th>Role</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Jane Doe</x-wirekit::table.td>
            <x-wirekit::table.td>Admin</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

## Sticky Header

Keep column headers visible while scrolling long tables:

:::preview{title="Sticky header"}
<x-wirekit::table sticky-header hoverable>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Name</x-wirekit::table.th>
            <x-wirekit::table.th>Role</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Balance</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row><x-wirekit::table.td>Alice</x-wirekit::table.td><x-wirekit::table.td>Admin</x-wirekit::table.td><x-wirekit::table.td align="right">€500</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Bob</x-wirekit::table.td><x-wirekit::table.td>Editor</x-wirekit::table.td><x-wirekit::table.td align="right">€1,200</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Carol</x-wirekit::table.td><x-wirekit::table.td>Viewer</x-wirekit::table.td><x-wirekit::table.td align="right">€890</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Dave</x-wirekit::table.td><x-wirekit::table.td>Editor</x-wirekit::table.td><x-wirekit::table.td align="right">€340</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Eve</x-wirekit::table.td><x-wirekit::table.td>Admin</x-wirekit::table.td><x-wirekit::table.td align="right">€2,100</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Frank</x-wirekit::table.td><x-wirekit::table.td>Viewer</x-wirekit::table.td><x-wirekit::table.td align="right">€750</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Grace</x-wirekit::table.td><x-wirekit::table.td>Admin</x-wirekit::table.td><x-wirekit::table.td align="right">€3,400</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Hank</x-wirekit::table.td><x-wirekit::table.td>Editor</x-wirekit::table.td><x-wirekit::table.td align="right">€620</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Ivy</x-wirekit::table.td><x-wirekit::table.td>Viewer</x-wirekit::table.td><x-wirekit::table.td align="right">€1,050</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Jack</x-wirekit::table.td><x-wirekit::table.td>Editor</x-wirekit::table.td><x-wirekit::table.td align="right">€980</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Karen</x-wirekit::table.td><x-wirekit::table.td>Admin</x-wirekit::table.td><x-wirekit::table.td align="right">€4,200</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>Leo</x-wirekit::table.td><x-wirekit::table.td>Viewer</x-wirekit::table.td><x-wirekit::table.td align="right">€310</x-wirekit::table.td></x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

The responsive wrapper gets `max-h-96` and `overflow-y-auto` automatically when `sticky-header` is enabled.

## Sticky First Column

Set `sticky-column` to freeze the leftmost column while the rest of the table scrolls horizontally — ideal for wide tables where the first column is the row's identity (a name, a date). The frozen column keeps a solid background so the scrolling cells don't show through; pair it with `sticky-header` to freeze the top-left corner too.

:::preview{title="Frozen first column"}
{{-- NO width wrapper here — the docs site pins any preview containing a
     data-wk-sticky-column table to the content column (its preview frame is
     otherwise fit-content and would grow to the 14-column table's full width).
     That generic rule keys off the data-wk-sticky-column marker the table
     component stamps, so the wide table overflows its own overflow-x-auto
     wrapper and the horizontal scroll shows. Don't re-add a fixed-width div:
     it's redundant, viewport-specific, and PreviewTableWidthWrapperGuardTest
     blocks the pattern. --}}
<x-wirekit::table sticky-column hoverable>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Region</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Jan</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Feb</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Mar</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Apr</x-wirekit::table.th>
            <x-wirekit::table.th align="right">May</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Jun</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Jul</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Aug</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Sep</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Oct</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Nov</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Dec</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Total</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row><x-wirekit::table.td>North region</x-wirekit::table.td><x-wirekit::table.td align="right">€12,400</x-wirekit::table.td><x-wirekit::table.td align="right">€11,800</x-wirekit::table.td><x-wirekit::table.td align="right">€13,200</x-wirekit::table.td><x-wirekit::table.td align="right">€12,900</x-wirekit::table.td><x-wirekit::table.td align="right">€14,100</x-wirekit::table.td><x-wirekit::table.td align="right">€13,800</x-wirekit::table.td><x-wirekit::table.td align="right">€15,200</x-wirekit::table.td><x-wirekit::table.td align="right">€14,600</x-wirekit::table.td><x-wirekit::table.td align="right">€15,900</x-wirekit::table.td><x-wirekit::table.td align="right">€16,300</x-wirekit::table.td><x-wirekit::table.td align="right">€15,600</x-wirekit::table.td><x-wirekit::table.td align="right">€18,200</x-wirekit::table.td><x-wirekit::table.td align="right">€174,000</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>South region</x-wirekit::table.td><x-wirekit::table.td align="right">€9,200</x-wirekit::table.td><x-wirekit::table.td align="right">€8,800</x-wirekit::table.td><x-wirekit::table.td align="right">€9,600</x-wirekit::table.td><x-wirekit::table.td align="right">€10,500</x-wirekit::table.td><x-wirekit::table.td align="right">€11,300</x-wirekit::table.td><x-wirekit::table.td align="right">€10,900</x-wirekit::table.td><x-wirekit::table.td align="right">€12,100</x-wirekit::table.td><x-wirekit::table.td align="right">€11,700</x-wirekit::table.td><x-wirekit::table.td align="right">€12,400</x-wirekit::table.td><x-wirekit::table.td align="right">€12,900</x-wirekit::table.td><x-wirekit::table.td align="right">€12,100</x-wirekit::table.td><x-wirekit::table.td align="right">€14,500</x-wirekit::table.td><x-wirekit::table.td align="right">€136,000</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>East region</x-wirekit::table.td><x-wirekit::table.td align="right">€7,600</x-wirekit::table.td><x-wirekit::table.td align="right">€7,200</x-wirekit::table.td><x-wirekit::table.td align="right">€8,100</x-wirekit::table.td><x-wirekit::table.td align="right">€8,400</x-wirekit::table.td><x-wirekit::table.td align="right">€9,000</x-wirekit::table.td><x-wirekit::table.td align="right">€8,700</x-wirekit::table.td><x-wirekit::table.td align="right">€9,500</x-wirekit::table.td><x-wirekit::table.td align="right">€9,100</x-wirekit::table.td><x-wirekit::table.td align="right">€9,800</x-wirekit::table.td><x-wirekit::table.td align="right">€10,200</x-wirekit::table.td><x-wirekit::table.td align="right">€9,700</x-wirekit::table.td><x-wirekit::table.td align="right">€11,800</x-wirekit::table.td><x-wirekit::table.td align="right">€109,100</x-wirekit::table.td></x-wirekit::table.row>
        <x-wirekit::table.row><x-wirekit::table.td>West region</x-wirekit::table.td><x-wirekit::table.td align="right">€10,800</x-wirekit::table.td><x-wirekit::table.td align="right">€10,300</x-wirekit::table.td><x-wirekit::table.td align="right">€11,200</x-wirekit::table.td><x-wirekit::table.td align="right">€11,600</x-wirekit::table.td><x-wirekit::table.td align="right">€12,400</x-wirekit::table.td><x-wirekit::table.td align="right">€12,000</x-wirekit::table.td><x-wirekit::table.td align="right">€13,100</x-wirekit::table.td><x-wirekit::table.td align="right">€12,700</x-wirekit::table.td><x-wirekit::table.td align="right">€13,500</x-wirekit::table.td><x-wirekit::table.td align="right">€14,000</x-wirekit::table.td><x-wirekit::table.td align="right">€13,300</x-wirekit::table.td><x-wirekit::table.td align="right">€15,900</x-wirekit::table.td><x-wirekit::table.td align="right">€150,800</x-wirekit::table.td></x-wirekit::table.row>
    </x-wirekit::table.body>
</x-wirekit::table>
:::

## With Footer

Use `<x-wirekit::table.foot>` for totals, summaries, or pagination rows:

:::preview{title="Table with footer"}
<x-wirekit::table>
    <x-wirekit::table.head>
        <x-wirekit::table.row>
            <x-wirekit::table.th>Product</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Qty</x-wirekit::table.th>
            <x-wirekit::table.th align="right">Price</x-wirekit::table.th>
        </x-wirekit::table.row>
    </x-wirekit::table.head>
    <x-wirekit::table.body>
        <x-wirekit::table.row>
            <x-wirekit::table.td>WireKit Pro License</x-wirekit::table.td>
            <x-wirekit::table.td align="right">2</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€198.00</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Priority Support</x-wirekit::table.td>
            <x-wirekit::table.td align="right">1</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€49.00</x-wirekit::table.td>
        </x-wirekit::table.row>
        <x-wirekit::table.row>
            <x-wirekit::table.td>Starter Kit</x-wirekit::table.td>
            <x-wirekit::table.td align="right">1</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€79.00</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.body>
    <x-wirekit::table.foot>
        <x-wirekit::table.row>
            <x-wirekit::table.td colspan="2">Total</x-wirekit::table.td>
            <x-wirekit::table.td align="right">€326.00</x-wirekit::table.td>
        </x-wirekit::table.row>
    </x-wirekit::table.foot>
</x-wirekit::table>
:::

## Responsive Behavior

By default the table is wrapped in a horizontal-scroll container with WireKit's themed scrollbar. Disable with `:responsive="false"` if you handle overflow yourself.

```blade
<x-wirekit::table :responsive="false">...</x-wirekit::table>
```

**A responsive table says it scrolls before you touch it.** A soft shadow appears at whichever inline edge has more table behind it, and disappears when you reach that end — so a reader on a phone, where there is no scrollbar until a drag is already underway, can see that the row continues instead of taking the cut-off column for the last one.

Nothing to switch on: it comes with `responsive`. It is driven by two one-pixel sentinels and an `IntersectionObserver`, so a hint is painted exactly while there is somewhere to scroll to — not on a timer, and not statically like a mask, which would dim the last column even once you had reached it. The pair is named for the start and end of the line rather than left and right, so it follows the reading direction: in a right-to-left document the hints sit on the other side.

The hint is a visual cue only. The scroll container itself carries `role="region"`, a label and `tabindex="0"`, so it is reachable and operable by keyboard whether or not the shadow is visible.

## Width & Layout

Tables fill their parent width. For narrow tables, constrain the wrapper:

```blade
<div class="max-w-2xl">
    <x-wirekit::table>…</x-wirekit::table>
</div>
```

When `responsive` is enabled (default), the table scrolls horizontally on small screens. Column widths can be controlled via Tailwind on `<x-wirekit::table.th>`:

```blade
<x-wirekit::table.th class="w-16">ID</x-wirekit::table.th>
<x-wirekit::table.th class="w-1/2">Name</x-wirekit::table.th>
```

## Props

### `<x-wirekit::table>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `striped` | bool | `false` | Alternating row backgrounds |
| `hoverable` | bool | `false` | Row highlight on hover |
| `compact` | bool | `false` | Reduced vertical padding |
| `responsive` | bool | `true` | Wrap in horizontal-scroll container |
| `stickyHeader` | bool | `false` | Sticky column headers while scrolling |
| `stickyColumn` | bool | `false` | Freeze the first column while the rest scrolls horizontally |
| `alpineSort` | bool | `false` | Enable client-side Alpine sorting (no Livewire needed) |
| `tableLabel` | string\|null | `null` | Accessible name for the focusable scroll region when `responsive` / `stickyHeader` makes the table keyboard-scrollable (WCAG 2.1.1). Falls back to the caption when present. |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::table.th>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `sortable` | bool | `false` | Render as clickable sortable header |
| `sortDirection` | string\|null | `null` | `'asc'`, `'desc'`, or `null` (Livewire mode) |
| `column` | string\|null | `null` | Column identifier for Alpine sort mode (pairs with `alpine-sort` on table) |
| `sortAction` | string\|null | `null` | Livewire-sort mode: the `wire:click` method call (e.g. `"sortBy('name')"`) that wraps the header label in a keyboard-operable `<button>` (WCAG 2.1.1 — the cursor-pointer cell alone is mouse-only). |
| `align` | string | `'left'` | `'left'`, `'center'`, `'right'` |
| `headerScope` | string | `'col'` | The `<th scope>` attribute: `'col'` (default), `'row'` for a per-row header cell (WCAG 1.3.1), `'colgroup'`, `'rowgroup'`. Distinct from `scope` below, which is theming. |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::table.td>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | string | `'left'` | `'left'`, `'center'`, `'right'` |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::table.caption>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::table.head>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | string | `'filled'` | `'filled'` paints the subtle header fill; `'flush'` drops it |
| `scope` | string\|null | `null` | Scoped personalization name |

`flush` removes the fill and **nothing else** — the bottom divider and the sticky-header rules stay, because those are what keep a head readable while the body scrolls under it. Reach for it when the head sits on a surface that already provides its own contrast; leave it alone otherwise.

### `<x-wirekit::table.body>`, `.foot`, `.row`

Only `scope` is available. All styling is driven by the parent table's data attributes.

## Accessibility

- `<th>` elements set `scope="col"` automatically — screen readers announce column headers as context for each cell
- Sortable columns expose `aria-sort="ascending" / "descending" / "none"` — see [WAI-ARIA Authoring Practices: Sortable Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/)
- The sort direction icon is `aria-hidden="true"` (decorative — the `aria-sort` value is the semantic signal)
- Hover tints rely on color, so avoid conveying row state through hover alone — use explicit status indicators (badges, icons with labels)

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus through column-header sort buttons (when `sortable`) and row action controls |
| `Enter` / `Space` (on a sort button) | Toggle the sort direction for that column |

*Cell content is delegated to the browser's native table semantics; assistive tech announces row / column relationships from the `<th>` / `<td>` markup.*

## Pitfalls

- **Don't put a `<x-wirekit::dropdown>` inside a `<table>` cell without testing.** Portal-rendered overlays escape the cell; ensure z-index and click-outside-to-close work in your specific layout.
- **Don't use HTML `<table>` for layout.** WCAG 1.3.1 — `<x-wirekit::table>` adds row/column ARIA semantics; misuse for layout is a screen-reader trap.

## Design Tokens

| Element | Token |
| --- | --- |
| Table background | inherits from parent |
| Head background | `--color-wk-bg-subtle` |
| Foot background | `--color-wk-bg-subtle` |
| Row divider | `--color-wk-border-subtle` |
| Head divider | `--color-wk-border` |
| Striped row tint | `--color-wk-bg-subtle` |
| Hover row tint | `--color-wk-bg-muted` |
| Cell padding | `--padding-wk-x-md` / `--padding-wk-y-md` |
| Compact cell padding | `--padding-wk-y-sm` |
| Text color | `--color-wk-text` |
| Header text color | `--color-wk-text-muted` |

## Customization

Override defaults without publishing views via `config/wirekit.php`:

```php
'components' => [
    'table' => [
        'striped' => true,
        'hoverable' => true,
        'compact' => false,
        'responsive' => true,
    ],
],
```

## Further Reading

- [WAI-ARIA Authoring Practices — Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/)
- [MDN: `<table>` HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table)
- [MDN: `aria-sort`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort)
