Skip to main content
WireKit
Copy for LLM

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

Simple table
Name Role Balance
Jane Doe Administrator €1,250.00
John Smith Editor €840.00

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.

Striped + hoverable + compact
Product SKU Stock
Keyboard KB-001 42
Mouse MS-014 128
Monitor MN-220 7

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.

Sortable headers (Alpine — interactive)
Name Created Status
Alpha 2025-01-15 Active
Beta 2025-03-08 Draft
Gamma 2025-02-22 Active

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-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:

Table with caption
A list of active team members and their roles.
Name Role
Jane Doe Admin

Keep column headers visible while scrolling long tables:

Sticky header
Name Role Balance
Alice Admin €500
Bob Editor €1,200
Carol Viewer €890
Dave Editor €340
Eve Admin €2,100
Frank Viewer €750
Grace Admin €3,400
Hank Editor €620
Ivy Viewer €1,050
Jack Editor €980
Karen Admin €4,200
Leo Viewer €310

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.

Frozen first column
Region Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Total
North region €12,400 €11,800 €13,200 €12,900 €14,100 €13,800 €15,200 €14,600 €15,900 €16,300 €15,600 €18,200 €174,000
South region €9,200 €8,800 €9,600 €10,500 €11,300 €10,900 €12,100 €11,700 €12,400 €12,900 €12,100 €14,500 €136,000
East region €7,600 €7,200 €8,100 €8,400 €9,000 €8,700 €9,500 €9,100 €9,800 €10,200 €9,700 €11,800 €109,100
West region €10,800 €10,300 €11,200 €11,600 €12,400 €12,000 €13,100 €12,700 €13,500 €14,000 €13,300 €15,900 €150,800

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

Table with footer
Product Qty Price
WireKit Pro License 2 €198.00
Priority Support 1 €49.00
Starter Kit 1 €79.00
Total €326.00

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 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
  • 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:

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

Further Reading

Was this page helpful?

Voting requires cookies or local storage. What we store