Skip to content
Praesepe UI

data-display

Table

Compound data table with keyboard-scrollable region, striped and hoverable rows, sortable headers, bulk select, and empty state.

Copied!

Examples

Orders table

Indonesian SME order list with badges and sortable amount.

Copied!
Invoice Customer Status
INV-2026-089 Toko Maju Jaya Paid Rp 2.450.000
INV-2026-090 CV Nusantara Pending USD 480.00
INV-2026-091 Warung Sederhana Draft Rp 125.000
<x-ui.table striped hoverable sticky-header label="Daftar pesanan">
    <x-ui.table.head>
        <x-ui.table.cell header>Invoice</x-ui.table.cell>
        <x-ui.table.cell header>Customer</x-ui.table.cell>
        <x-ui.table.cell header>Status</x-ui.table.cell>
        <x-ui.table.cell header sortable sort="descending" numeric>Total</x-ui.table.cell>
    </x-ui.table.head>
    <x-ui.table.body>
        <x-ui.table.row>
            <x-ui.table.cell numeric>INV-2026-089</x-ui.table.cell>
            <x-ui.table.cell>Toko Maju Jaya</x-ui.table.cell>
            <x-ui.table.cell><x-ui.badge color="success" size="sm">Paid</x-ui.badge></x-ui.table.cell>
            <x-ui.table.cell numeric>Rp 2.450.000</x-ui.table.cell>
        </x-ui.table.row>
        <x-ui.table.row>
            <x-ui.table.cell numeric>INV-2026-090</x-ui.table.cell>
            <x-ui.table.cell>CV Nusantara</x-ui.table.cell>
            <x-ui.table.cell><x-ui.badge color="warning" size="sm">Pending</x-ui.badge></x-ui.table.cell>
            <x-ui.table.cell numeric>USD 480.00</x-ui.table.cell>
        </x-ui.table.row>
        <x-ui.table.row>
            <x-ui.table.cell numeric>INV-2026-091</x-ui.table.cell>
            <x-ui.table.cell>Warung Sederhana</x-ui.table.cell>
            <x-ui.table.cell><x-ui.badge color="info" size="sm">Draft</x-ui.badge></x-ui.table.cell>
            <x-ui.table.cell numeric>Rp 125.000</x-ui.table.cell>
        </x-ui.table.row>
    </x-ui.table.body>
</x-ui.table>
<div class="overflow-x-auto rounded-card border border-border" role="region" aria-label="Daftar pesanan" tabindex="0">
    <table class="w-full min-w-[40rem] border-collapse text-left text-sm [&_tbody_tr]:transition-colors [&_tbody_tr:hover]:bg-surface-sunken/50">
        <thead class="bg-surface-sunken/60 text-xs font-semibold uppercase tracking-wider text-fg-muted">
            <tr>
                <th class="px-4 py-2.5 font-semibold">Invoice</th>
                <th class="px-4 py-2.5 font-semibold">Customer</th>
                <th class="px-4 py-2.5 font-semibold">Status</th>
                <th class="px-4 py-2.5 font-semibold">
                    <button type="button" class="inline-flex items-center gap-1 uppercase tracking-wider" aria-sort="descending">Total <span aria-hidden="true">↓</span></button>
                </th>
            </tr>
        </thead>
        <tbody class="divide-y divide-border bg-surface-raised">
            <tr>
                <td class="px-4 py-3 tabular-nums">INV-2026-089</td>
                <td class="px-4 py-3">Toko Maju Jaya</td>
                <td class="px-4 py-3"><span class="inline-flex items-center rounded-pill bg-success-soft px-2 py-0.5 text-xs font-medium tabular-nums text-success shadow-2xs">Paid</span></td>
                <td class="px-4 py-3 tabular-nums">Rp 2.450.000</td>
            </tr>
            <tr>
                <td class="px-4 py-3 tabular-nums">INV-2026-090</td>
                <td class="px-4 py-3">CV Nusantara</td>
                <td class="px-4 py-3"><span class="inline-flex items-center rounded-pill bg-warning-soft px-2 py-0.5 text-xs font-medium tabular-nums text-warning shadow-2xs">Pending</span></td>
                <td class="px-4 py-3 tabular-nums">USD 480.00</td>
            </tr>
            <tr>
                <td class="px-4 py-3 tabular-nums">INV-2026-091</td>
                <td class="px-4 py-3">Warung Sederhana</td>
                <td class="px-4 py-3"><span class="inline-flex items-center rounded-pill bg-info-soft px-2 py-0.5 text-xs font-medium tabular-nums text-info shadow-2xs">Draft</span></td>
                <td class="px-4 py-3 tabular-nums">Rp 125.000</td>
            </tr>
        </tbody>
    </table>
</div>

Bulk select

Header and row checkboxes driven by Alpine.

Copied!
Invoice Due date Amount
INV-2026-089 28 Oct 2026 Rp 2.450.000
INV-2026-090 02 Nov 2026 USD 480.00
<x-ui.table selectable label="Faktur terpilih">
    <x-ui.table.head>
        <x-ui.table.cell header select-all />
        <x-ui.table.cell header>Invoice</x-ui.table.cell>
        <x-ui.table.cell header>Due date</x-ui.table.cell>
        <x-ui.table.cell header numeric>Amount</x-ui.table.cell>
    </x-ui.table.head>
    <x-ui.table.body>
        <x-ui.table.row id="INV-2026-089">
            <x-ui.table.cell select-id="INV-2026-089" />
            <x-ui.table.cell numeric>INV-2026-089</x-ui.table.cell>
            <x-ui.table.cell numeric>28 Oct 2026</x-ui.table.cell>
            <x-ui.table.cell numeric>Rp 2.450.000</x-ui.table.cell>
        </x-ui.table.row>
        <x-ui.table.row id="INV-2026-090">
            <x-ui.table.cell select-id="INV-2026-090" />
            <x-ui.table.cell numeric>INV-2026-090</x-ui.table.cell>
            <x-ui.table.cell numeric>02 Nov 2026</x-ui.table.cell>
            <x-ui.table.cell numeric>USD 480.00</x-ui.table.cell>
        </x-ui.table.row>
    </x-ui.table.body>
</x-ui.table>
<div x-data="{ selected: [], toggle(id) { this.selected = this.selected.includes(id) ? this.selected.filter(v => v !== id) : [...this.selected, id] } }">
    <div class="overflow-x-auto rounded-card border border-border" role="region" aria-label="Faktur terpilih" tabindex="0">
        <table class="w-full min-w-[32rem] text-left text-sm [&_tbody_tr]:transition-colors [&_tbody_tr:hover]:bg-surface-sunken/50">
            <thead class="bg-surface-sunken/60 text-xs font-semibold uppercase tracking-wider text-fg-muted">
                <tr>
                    <th class="px-4 py-2.5"><span class="sr-only">Select</span></th>
                    <th class="px-4 py-2.5 font-semibold">Invoice</th>
                    <th class="px-4 py-2.5 font-semibold">Due date</th>
                    <th class="px-4 py-2.5 font-semibold">Amount</th>
                </tr>
            </thead>
            <tbody class="divide-y divide-border">
                <tr>
                    <td class="px-4 py-3"><input type="checkbox" x-on:change="toggle('INV-2026-089')" aria-label="Select INV-2026-089"></td>
                    <td class="px-4 py-3 tabular-nums">INV-2026-089</td>
                    <td class="px-4 py-3 tabular-nums">28 Oct 2026</td>
                    <td class="px-4 py-3 tabular-nums">Rp 2.450.000</td>
                </tr>
                <tr>
                    <td class="px-4 py-3"><input type="checkbox" x-on:change="toggle('INV-2026-090')" aria-label="Select INV-2026-090"></td>
                    <td class="px-4 py-3 tabular-nums">INV-2026-090</td>
                    <td class="px-4 py-3 tabular-nums">02 Nov 2026</td>
                    <td class="px-4 py-3 tabular-nums">USD 480.00</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

Empty state

Clean empty table with action slot.

Copied!
SKU Stock

No products match

Try Gudang A / Bandung or clear the SKU filter.

<x-ui.table label="Hasil pencarian kosong">
    <x-ui.table.head>
        <x-ui.table.cell header>SKU</x-ui.table.cell>
        <x-ui.table.cell header>Stock</x-ui.table.cell>
    </x-ui.table.head>
    <x-ui.table.body>
        <x-ui.table.empty
            title="No products match"
            description="Try Gudang A / Bandung or clear the SKU filter."
        >
            <x-slot:action>
                <x-ui.button size="sm">Add product</x-ui.button>
            </x-slot:action>
        </x-ui.table.empty>
    </x-ui.table.body>
</x-ui.table>
<div class="overflow-x-auto rounded-card border border-border" role="region" aria-label="Hasil pencarian kosong" tabindex="0">
    <table class="w-full text-sm">
        <thead class="bg-surface-sunken/60 text-xs font-semibold uppercase tracking-wider text-fg-muted">
            <tr>
                <th class="px-4 py-2.5 font-semibold">SKU</th>
                <th class="px-4 py-2.5 font-semibold">Stock</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td colspan="2" class="px-4 py-12 text-center">
                    <p class="text-sm font-semibold tracking-tight text-fg">No products match</p>
                    <p class="mt-1 text-sm text-fg-muted leading-relaxed">Try Gudang A / Bandung or clear the SKU filter.</p>
                </td>
            </tr>
        </tbody>
    </table>
</div>

Props API

Component props
Prop Type Default Description
striped bool false Alternating row backgrounds.
hoverable bool false Highlight rows on hover.
selectable bool false Enables Alpine bulk-select state shared by header and row checkboxes.
label string Data table Accessible name for the scrollable region wrapper.
cell.sortable / cell.sort bool / none|ascending|descending false / none Sortable header button with aria-sort state.

Accessibility notes

Architected with WCAG 2.2 AA patterns in mind. These notes document the exact keyboard and ARIA behavior for this component.

  • The overflow wrapper uses role="region", aria-label, and tabindex="0" so keyboard users can scroll wide tables.
  • Sortable headers are buttons with aria-sort values: none, ascending, or descending.
  • Bulk select checkboxes include accessible names. The header checkbox reflects indeterminate state when some rows are selected.
  • Empty state keeps table structure with a single spanning cell, icon, description, and optional action.

Built by Praesepe

Architecture & custom software partner

Praesepe uses these exact UI primitives to ship SME dashboards, inventory systems, invoicing desks, and internal tools faster, without rebuilding a design system for every client.

  • Same Blade + Tailwind 4 + Alpine 3 stack you see in this docs site
  • Opinionated for Indonesian SME ops: invoices, stock, roles, approvals
  • Architected with WCAG 2.2 AA patterns in mind for keyboard and ARIA

Talk to the studio

WhatsApp: TODO Email the studio Visit Praesepe