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 aconsole.warnin 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 thetable.tdsub-component uses — so a raw table is no longer cramped flush-to-border even before you migrate it to the sub-components.
Basic Usage
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.
Variant flags compose freely — any combination of striped, hoverable, and compact works. Example:
<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.
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:
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.
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.
<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.
<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-sortis 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:
Sticky Header
Keep column headers visible while scrolling long tables:
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.
With Footer
Use <x-wirekit::table.foot> for totals, summaries, or pagination rows:
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.
<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:
<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>:
<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 setscope="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 - The sort direction icon is
aria-hidden="true"(decorative — thearia-sortvalue 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:
'components' => [
'table' => [
'striped' => true,
'hoverable' => true,
'compact' => false,
'responsive' => true,
],
],