Data Table Page

Production-ready data table layouts with filtering, sorting, and bulk actions.

Overview

Data table pages are the main operating surface for SaaS dashboards, admin panels, ERP/POS systems, ecommerce back offices, CRM tools, inventory systems, and internal tools. They help users scan records, search and filter, sort and compare, select rows, perform bulk actions, and navigate large datasets with pagination.

Byteion provides both a low-level Table primitive for custom markup and a high-level DataTable component with built-in search, sorting, pagination, row selection, and toolbar integration. This pattern explains how to compose them into production-ready data pages.

Data Table Page is a pattern, not a single component. Use it as a blueprint for composing Byteion primitives — Table or DataTable, Input, Select, Button, Badge, Checkbox, Pagination, Dropdown, Alert, Skeleton, Empty state, Modal, and Toast — into complete data management screens.

What this pattern includes

  • Page title and description
  • Primary action button (e.g. "New customer")
  • Search input — text-based record filtering
  • Filters — structured field selectors (status, role, category, date range)
  • Sort controls — sortable column headers with direction indicator
  • Row selection — checkboxes per row with select-all in header
  • Bulk action bar — appears when rows are selected, shows count and actions
  • Data table — structured row/column display
  • Status badges — colour-coded status indicators with text labels
  • Row actions — view, edit, duplicate, delete per row
  • Pagination — page navigation with rows-per-page selector
  • Rows-per-page selector
  • Result count — "Showing 1–10 of 32 customers"
  • Loading state — skeleton placeholders
  • Empty state — no records message
  • Error state — error message with retry action
  • Mobile responsive layout — horizontal scroll or card view

Common use cases

User / Customer Management

View, search, filter, and manage user accounts. Bulk enable/disable, change roles, send invites.

Order Management

Track orders by status, date, customer. Filter by fulfillment stage, bulk update status.

Product Catalog

Manage inventory, prices, categories. Search by SKU or name, filter by stock status.

Inventory List

Stock levels, warehouse locations, reorder thresholds. Bulk transfer or adjust stock.

Invoice / Payment Records

Search by invoice number, filter by status, sort by amount or date. Bulk resend or cancel.

Subscription List

View active/churned/trialling subscriptions. Filter by plan, sort by MRR.

Audit Logs

Chronological action log with user, action, resource, timestamp filters. Read-only usually.

Support Tickets

Filter by status, priority, assignee. Bulk assign, sort by created date.

Booking / Reservation List

View bookings by date range, status, resource. Bulk confirm or cancel.

POS Sales History

Point-of-sale transaction log with payment method, amount, cashier. Search by receipt number.

ERP Reports

Procurement, ledger, and operational reports with custom date ranges and filter dimensions.

Team / Member Management

Manage team members, roles, permissions. Bulk invite or remove.

When to use

  • Large record lists that need scanning, searching, and filtering
  • Admin CRUD screens
  • Searchable and filterable datasets
  • Records requiring row-level actions (view, edit, delete)
  • Records requiring bulk actions (archive, delete, assign)
  • Server-side paginated data
  • Operational dashboards

When not to use

  • Very small lists that fit in a few lines — use List instead
  • Visual content grids (image galleries, card-based layouts)
  • Feed or timeline content — use Timeline instead
  • Step-by-step workflows — use Stepper instead
  • Data that needs chart-first analysis — use KPI widgets and charts instead
  • Complex comparison views where all items must be visible together on one screen

Full UI example

Customers page with title, search, status filter, new customer button, selected-row bulk action bar, data table with status badges, row actions, and pagination:

Customers

Manage your customer accounts.

2 selected
Showing 1–10 of 32 customers
Name Email Status Role Actions
Olivia Martin olivia.martin@email.com Active Admin
Jackson Lee jackson.lee@email.com Pending Member
Isabella Chen isabella.chen@email.com Active Member
Rows per page:

HTML example

Complete data table page with accessible markup and semantic tokens:

<!-- Page header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-4">
  <div>
    <h1 class="text-xl font-semibold text-foreground">Customers</h1>
    <p class="text-sm text-muted-foreground">Manage your customer accounts.</p>
  </div>
  <div class="flex flex-col sm:flex-row gap-3">
    <div class="relative">
      <i class="ti ti-search absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm" aria-hidden="true"></i>
      <input type="search" placeholder="Search customers…" aria-label="Search customers"
             class="h-10 w-full min-w-[220px] rounded-lg border border-border bg-card pl-9 pr-3.5
                    text-sm text-foreground shadow-field outline-none
                    placeholder:text-muted-foreground
                    focus-visible:border-primary-400 focus-visible:shadow-field-focus">
    </div>
    <button class="inline-flex items-center justify-center gap-2 h-10 rounded-lg
                   bg-primary-600 px-4 text-sm font-medium text-white shadow-sm
                   hover:bg-primary-700 transition">
      <i class="ti ti-plus text-base" aria-hidden="true"></i> New customer
    </button>
  </div>
</div>

<!-- Filters -->
<div class="flex flex-wrap items-center gap-3 mb-4">
  <select aria-label="Filter by status"
          class="h-9 rounded-lg border border-border bg-card px-3 text-sm text-foreground
                 shadow-field outline-none
                 focus-visible:border-primary-400 focus-visible:shadow-field-focus">
    <option>All statuses</option><option>Active</option><option>Inactive</option>
  </select>
</div>

<!-- Bulk action bar -->
<div class="flex items-center justify-between px-4 py-3 rounded-lg border border-border bg-muted/30 mb-4">
  <div class="flex items-center gap-2 text-sm font-medium text-foreground">
    <span>2 selected</span>
  </div>
  <div class="flex items-center gap-2">
    <button class="inline-flex items-center justify-center h-8 rounded-lg
                   border border-border bg-background px-3 text-sm font-medium
                   text-foreground shadow-sm hover:bg-muted transition">
      Archive
    </button>
    <button class="inline-flex items-center justify-center h-8 rounded-lg
                   bg-danger-600 px-3 text-sm font-medium text-white shadow-sm
                   hover:bg-danger-700 transition">
      Delete
    </button>
  </div>
</div>

<!-- Table -->
<div class="overflow-x-auto rounded-xl border border-border">
  <table class="w-full text-sm">
    <thead>
      <tr class="border-b border-border bg-muted">
        <th class="w-12 px-4 py-3 text-left" scope="col">
          <input type="checkbox" class="h-4 w-4 rounded border-border accent-primary-600"
                 aria-label="Select all rows">
        </th>
        <th class="px-4 py-3 text-left font-medium text-foreground" scope="col"
            aria-sort="ascending">
          Name <i class="ti ti-arrow-up text-xs" aria-hidden="true"></i>
        </th>
        <th class="px-4 py-3 text-left font-medium text-foreground" scope="col">Email</th>
        <th class="px-4 py-3 text-left font-medium text-foreground" scope="col">Status</th>
        <th class="px-4 py-3 text-left font-medium text-foreground" scope="col">Role</th>
        <th class="w-20 px-4 py-3 text-right font-medium text-foreground" scope="col">Actions</th>
      </tr>
    </thead>
    <tbody class="divide-y divide-border">
      <tr class="hover:bg-muted/50 transition">
        <td class="px-4 py-3">
          <input type="checkbox" class="h-4 w-4 rounded border-border accent-primary-600"
                 aria-label="Select Olivia Martin">
        </td>
        <td class="px-4 py-3 font-medium text-foreground">Olivia Martin</td>
        <td class="px-4 py-3 text-muted-foreground">olivia.martin@email.com</td>
        <td class="px-4 py-3">
          <span class="inline-flex items-center gap-1.5 rounded-full bg-success-50 px-2.5 py-0.5
                       text-xs font-medium text-success-700">Active</span>
        </td>
        <td class="px-4 py-3 text-muted-foreground">Admin</td>
        <td class="px-4 py-3 text-right">
          <button class="p-1 rounded-lg hover:bg-muted text-muted-foreground
                         hover:text-foreground transition"
                  aria-label="Actions for Olivia Martin">
            <i class="ti ti-dots-vertical" aria-hidden="true"></i>
          </button>
        </td>
      </tr>
    </tbody>
  </table>
</div>

<!-- Pagination -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mt-4">
  <p class="text-sm text-muted-foreground">Showing 1–10 of 32 customers</p>
  <nav class="flex items-center gap-2" aria-label="Pagination">
    <button class="inline-flex items-center justify-center h-8 w-8 rounded-lg
                   border border-border bg-background text-foreground
                   hover:bg-muted transition disabled:opacity-40"
            disabled aria-label="Previous page">
      <i class="ti ti-chevron-left text-sm" aria-hidden="true"></i>
    </button>
    <button class="inline-flex items-center justify-center h-8 w-8 rounded-lg
                   bg-primary-600 text-white text-sm font-medium">1</button>
    <button class="inline-flex items-center justify-center h-8 w-8 rounded-lg
                   border border-border bg-background text-foreground
                   hover:bg-muted transition text-sm">2</button>
    <button class="inline-flex items-center justify-center h-8 w-8 rounded-lg
                   border border-border bg-background text-foreground
                   hover:bg-muted transition disabled:opacity-40"
            aria-label="Next page">
      <i class="ti ti-chevron-right text-sm" aria-hidden="true"></i>
    </button>
  </nav>
</div>

React example

Using DataTable with search, sorting, pagination, row selection, toolbar, and empty state. For custom table markup, use Table with its compound sub-components:

import { DataTable, Button, Badge, Dropdown, Modal, Toast, EmptyState } from '@byteion/react'
import { useState } from 'react'

interface Customer {
  id: string
  name: string
  email: string
  status: 'active' | 'pending' | 'inactive'
  role: string
}

const columns: DataTableColumn[] = [
  { key: 'name',  label: 'Name',   sortAccessor: (r) => r.name },
  { key: 'email', label: 'Email' },
  {
    key: 'status', label: 'Status',
    render: (r) => (
      <Badge tone={r.status === 'active' ? 'success' : r.status === 'pending' ? 'warning' : 'neutral'}>
        {r.status}
      </Badge>
    ),
    sortAccessor: (r) => r.status,
  },
  { key: 'role',  label: 'Role',  sortAccessor: (r) => r.role },
  {
    key: 'actions', label: '',
    align: 'right',
    render: (r) => (
      <Dropdown>
        <Dropdown.Item onClick={() => handleEdit(r)}>Edit</Dropdown.Item>
        <Dropdown.Item onClick={() => handleDelete(r)} tone="danger">Delete</Dropdown.Item>
      </Dropdown>
    ),
  },
]

function CustomersPage() {
  const [selected, setSelected] = useState<string[]>([])
  const [deleting, setDeleting] = useState(false)

  const handleBulkDelete = async () => {
    setDeleting(true)
    try {
      await fetch('/api/customers/bulk-delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ids: selected }),
      })
      setSelected([])
      Toast.success(`${selected.length} customers deleted`)
    } catch {
      Toast.error('Failed to delete some customers')
    } finally {
      setDeleting(false)
    }
  }

  return (
    <div className="space-y-4">
      <h1 className="text-xl font-semibold text-foreground">Customers</h1>

      <DataTable
        data={data}
        columns={columns}
        rowKey={(r) => r.id}
        selectable
        selectedRowIds={selected}
        onSelectionChange={setSelected}
        searchPlaceholder="Search customers…"
        toolbarRight={
          <Button onClick={() => /* open create modal */}>
            New customer
          </Button>
        }
        emptyState={
          <EmptyState
            title="No customers yet"
            description="Get started by importing your customer data."
          />
        }
      />

      {selected.length > 0 && (
        <div className="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3">
          <span className="text-sm font-medium text-foreground">
            {selected.length} selected
          </span>
          <div className="flex gap-2">
            <Button variant="outline">Archive</Button>
            <Button tone="danger" onClick={handleBulkDelete} loading={deleting}>
              Delete
            </Button>
          </div>
        </div>
      )}

      <Modal open={confirmOpen} onClose={() => setConfirmOpen(false)}>
        <Modal.Header>Delete {customer?.name}?</Modal.Header>
        <Modal.Body>This action cannot be undone.</Modal.Body>
        <Modal.Footer>
          <Button variant="outline" onClick={() => setConfirmOpen(false)}>Cancel</Button>
          <Button tone="danger" loading={deleting}>Delete</Button>
        </Modal.Footer>
      </Modal>
    </div>
  )
}

Vue example

Using BiDataTable with v-model for search, sort, page, and selection. For custom table markup, use BiTable with BiTableHead, BiTableBody, BiTableRow, BiHeaderCell, and BiTableCell:

<template>
  <div class="space-y-4">
    <h1 class="text-xl font-semibold text-foreground">Orders</h1>

    <BiDataTable
      :data="orders"
      :columns="columns"
      row-key="id"
      selectable
      v-model:selected-row-ids="selected"
      search-placeholder="Search orders…"
    >
      <template #toolbar-right>
        <BiButton @click="openCreate">New order</BiButton>
      </template>
      <template #empty>
        <BiEmptyState title="No orders yet"
          description="Create your first order to get started." />
      </template>
    </BiDataTable>

    <!-- Bulk action bar -->
    <div v-if="selected.length > 0"
         class="flex items-center justify-between rounded-lg border border-border bg-muted/30 px-4 py-3">
      <span class="text-sm font-medium text-foreground">
        {{ selected.length }} selected
      </span>
      <div class="flex gap-2">
        <BiButton variant="outline" @click="bulkArchive">Archive</BiButton>
        <BiButton tone="danger" :loading="deleting" @click="confirmDelete">Delete</BiButton>
      </div>
    </div>

    <!-- Delete confirmation modal -->
    <BiModal v-model:open="confirmOpen">
      <h3 class="font-semibold text-foreground">Delete {{ selected.length }} order(s)?</h3>
      <p class="text-sm text-muted-foreground mt-2">This action cannot be undone.</p>
      <template #footer>
        <div class="flex justify-end gap-3">
          <BiButton variant="outline" @click="confirmOpen = false">Cancel</BiButton>
          <BiButton tone="danger" :loading="deleting" @click="bulkDelete">Delete</BiButton>
        </div>
      </template>
    </BiModal>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { BiDataTable, BiButton, BiBadge, BiModal, BiEmptyState, BiToast } from '@byteion/vue'
import type { DataTableColumn } from '@byteion/vue'

interface Order { id: string; customer: string; total: number; status: string }

const columns: DataTableColumn<Order>[] = [
  { key: 'customer', label: 'Customer', sortAccessor: (r) => r.customer },
  { key: 'total', label: 'Total', sortAccessor: (r) => r.total,
    render: (r) => `$${r.total.toFixed(2)}` },
  { key: 'status', label: 'Status',
    render: (r) => <BiBadge :tone="r.status === 'completed' ? 'success' : 'warning'">{{ r.status }}</BiBadge>,
    sortAccessor: (r) => r.status },
]

const orders = ref<Order[]>([])
const selected = ref<string[]>([])
const deleting = ref(false)
const confirmOpen = ref(false)

async function bulkDelete() {
  deleting.value = true
  try {
    await fetch('/api/orders/bulk-delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ids: selected.value }),
    })
    BiToast.success(`${selected.value.length} orders deleted`)
    selected.value = []
    confirmOpen.value = false
  } catch {
    BiToast.error('Failed to delete some orders')
  } finally {
    deleting.value = false
  }
}
</script>

Table anatomy

  1. Page header — page title, description, primary action button.
  2. Toolbar — search input + filter controls + action buttons.
  3. Search input — broad text matching across records.
  4. Filters — structured field selectors (status, role, category, date range).
  5. Bulk action bar — appears when rows are selected; shows count and bulk action buttons.
  6. Table container — overflow-x-auto wrapper for horizontal scroll on small screens.
  7. Header row — <thead> with <th scope="col"> columns.
  8. Sortable column headers — clickable headers with aria-sort and direction indicator.
  9. Row selection checkboxes — checkbox per row + select-all in header.
  10. Data rows — <tbody> with row data and hover state.
  11. Status cells — colour-coded badges with text labels.
  12. Row action menu — dropdown or icon button for view, edit, delete per row.
  13. Pagination footer — page navigation with page numbers, prev/next buttons.
  14. Rows-per-page selector — dropdown to change page size.
  15. Result count — "Showing 1–10 of 32 customers."

Search and filtering

  • Use search for broad text matching across multiple fields. Place it prominently above the table.
  • Use filters for structured fields like status, role, category, and date range. Place filters adjacent to the search input or in a collapsible filter panel.
  • Preserve search and filter state in URL query when deep linking or browser navigation matters (e.g., /customers?search=olivia&status=active).
  • Reset page to 1 when search or filters change — paginating into a filtered subset is confusing.
  • Debounce search when it triggers backend requests — wait 300–400ms after the user stops typing.
  • Show active filter chips if multiple filters are applied, with a "Clear all" action.
  • Show a filtered empty state when no records match the current filters — differentiate from the fully empty state.

Sorting

  • Make columns sortable only where sorting is meaningful — name, email, date, amount, status. Avoid sorting columns with mixed or ambiguous values.
  • Show the current sort direction with an icon (up arrow for ascending, down arrow for descending).
  • Use aria-sort="ascending" or aria-sort="descending" on the active sortable header.
  • Preserve sort across pagination — changing the page should not reset the sort order.
  • Server-side sorting is recommended for large datasets. Client-side sorting works only when all data is loaded.

Pagination and data loading

  • Use server-side pagination for large datasets — never load all records into the browser just to paginate on the client.
  • Preserve search, filters, sort, and page in the URL or application state for consistency.
  • Show a rows-per-page selector when users need control over density (10, 25, 50, 100).
  • Show the result count: "Showing 1–10 of 32 customers."
  • Use loading state (skeleton placeholders) while fetching data.
  • Use empty state when no records exist (before any filters are applied).
  • Use error state with a retry action when data loading fails.
  • Test page 1, middle page, last page, totalPages=1, and empty data responses.
  • Use the Pagination component with page, totalPages, and onPageChange props. Variants: numbered (default), prev-next, compact.

Bulk actions

  • Show the bulk action bar only when rows are selected — hide it when no rows are selected.
  • Show the selected row count in the bar: "3 selected".
  • Keep selected rows visually clear with a highlighted background.
  • Require confirmation for destructive actions — use a Modal to confirm before bulk delete, archive, or any irreversible operation.
  • Disable bulk buttons during processing — show a loading indicator and prevent duplicate submissions.
  • Clear selection after success — reset selected rows after a successful bulk operation.
  • Handle partial failures — if some records succeed and some fail, show a clear message differentiating the results.
  • Do not keep selections across filter or page changes unless intentionally designed as a multi-page selection flow.
  • Never run destructive actions without confirmation — a single click should never delete or mutate records.

Row actions

  • Use row actions for single-record operations: view, edit, duplicate, archive, delete.
  • Keep the primary row action predictable — clicking the row or its primary identifier should navigate to the detail view.
  • Put secondary actions in a Dropdown or <button> menu to keep the table uncluttered.
  • Use accessible labels for row action buttons: "Edit Olivia Martin", "Delete order #1248".
  • Confirm destructive row actions with a modal, just like bulk actions.

States

Default

Normal table display with data rows, pagination, and all controls visible.

Loading

Skeleton placeholders while data is being fetched. Toolbar remains visible.

Empty

No records exist. Show an illustration, message, and optional CTA.

Filtered empty

No records match the current filters. Differentiate from the fully empty state. Show "Clear filters" action.

Error

Data loading failed. Show error message with a Retry button.

Selected

One or more rows selected. Bulk action bar visible with count and actions.

Saving / Processing

Bulk or row action in progress. Buttons disabled with loading state.

Partial failure

Some items succeeded, some failed. Show a clear breakdown of results.

Accessibility

  • Use semantic <table>, <thead>, <tbody>, <tr>, <th scope="col">, and <td> elements.
  • Provide a heading or caption that identifies the table purpose.
  • Use aria-sort on sortable column headers: "ascending", "descending", or "none".
  • Give row selection checkboxes accessible names using aria-label — "Select Olivia Martin".
  • Keep keyboard focus visible — never remove outline without an alternative focus indicator.
  • Do not rely only on badge colour for status — use text labels inside badges (e.g. "Active" not just a green dot).
  • Use descriptive aria-label for row action buttons — "Actions for Olivia Martin" rather than just "Actions".
  • Wrap pagination in <nav aria-label="Pagination">. Individual page buttons should be accessible by keyboard.
  • Announce loading and error states with accessible text where appropriate.
  • Keep focus predictable after filtering, pagination, and bulk actions — do not move focus unexpectedly.

Responsive behaviour

  • Use horizontal scroll inside the table container (overflow-x-auto) for dense tables on small screens. Byteion's Table component does this by default (wrapped prop).
  • Consider a card view for mobile-first workflows where horizontal scroll is impractical — each row becomes a card on mobile.
  • Keep the primary identifier column visible (first data column).
  • Hide secondary columns on small screens if needed — use responsive utility classes (hidden sm:table-cell).
  • Move row actions into a compact menu on mobile — an icon button is sufficient.
  • Keep bulk actions accessible on small screens — the bulk action bar should be full-width.
  • Avoid more than 5–6 visible columns on mobile — prioritise the most important data.
  • Prevent horizontal page overflow — only the table container should scroll, not the entire page.

Performance guidance

  • Prefer server-side pagination, filtering, and sorting for large datasets. The backend handles data processing; the UI only renders one page.
  • Use virtualisation only when many rows must render in a single view (e.g. 1000+ rows visible at once). For normal paginated tables, server-side pagination is simpler and more performant.
  • Debounce backend search — wait 300–400ms after the user stops typing before sending a search request.
  • Avoid rendering expensive cell components (rich editors, charts) for every row — keep cells lightweight.
  • Memoise derived rows in React/Vue when filtering or sorting large client-side datasets.
  • Cache query results where appropriate — avoid re-fetching the same page if the user navigates away and back.
  • Do not load all records into the browser just to paginate on the client for datasets over 500 rows.

Security and permissions

  • Backend must enforce permissions for every row action and bulk action — never rely on UI-only hiding.
  • Hide or disable actions based on user permissions, but always validate server-side.
  • Confirm destructive actions with a modal dialog — single clicks should never delete records.
  • Audit important admin actions — bulk delete, role changes, data export should be logged server-side.
  • Avoid exposing sensitive columns unnecessarily (full card numbers, personal data).
  • Mask sensitive data where needed — show only last 4 digits of phone numbers or partial email addresses.

Component breakdown

Table

Semantic <table> primitive with striped, bordered, and wrapped props. Compound sub-components: Table.Head, Table.Body, Table.Row, Table.HeaderCell, Table.Cell.

DataTable

Opinionated data grid with built-in search, sorting, pagination, row selection, and toolbar. Controlled via search, sort, page, selectedRowIds with on*Change callbacks.

Input

Search field with leading icon, type="search", accessible label, and shadow-field styling.

Select

Filter dropdowns for status, role, category, date range, and rows-per-page selector.

Button

Primary action button (New record), bulk action buttons (Archive, Delete), and pagination buttons. Supports loading and tone="danger".

Badge

Status indicators with semantic tone colours and text labels. Use tone prop (success, warning, danger, neutral).

Checkbox

Row selection and select-all with aria-label for accessible names.

Pagination

Page navigation with page, totalPages, onPageChange. Variants: numbered, prev-next, compact.

Dropdown / Menu

Row action menus with Edit, Duplicate, Delete options. Supports items with tone="danger" for destructive actions.

Alert

Error notifications and success confirmations within the table page.

Skeleton / Loading

Loading placeholders for table rows while data is being fetched.

Empty State

Empty dataset display with illustration, title, description, and optional CTA.

Modal / Dialog

Confirmation dialogs for destructive bulk and row actions.

Toast

Non-intrusive success/error feedback after bulk or row actions.

Layout variations

Standard table page

Default layout with toolbar, table, and pagination. Suitable for user management, subscription lists, and CRM records.

Filter-heavy table

Multiple filter controls in a collapsible panel above the table. Common in order management, audit logs, and ERP reports.

Bulk-action table

Prominent bulk action bar for batch operations. Common in ecommerce back offices, inventory, and team management.

Mobile card view

Data displayed as cards instead of table rows on small screens. Each card shows the most important fields for the record.

Expandable rows

Rows that expand inline to show additional details or related data. Common in order details, invoice items, and project tasks.

Inline edit table

Editable fields directly in the table without navigating to a separate form. Common in inventory management, spreadsheet-like tools, and quick-edit workflows.

Virtualised table

For thousands of rows rendered in a single view (not paginated). Uses virtual scrolling to render only visible rows. Use when normal server pagination is not appropriate.

Audit log table

Chronological, read-only table with timestamp, user, action, and resource columns. Often filter-heavy with date range and action type filters. No inline editing.

Best practices

Do

  • Put search and filters above the table
  • Show the result count
  • Preserve search, filter, and sort during pagination
  • Use semantic tokens for all surfaces, borders, and text
  • Use status badges with text labels, not colour alone
  • Confirm destructive actions with a modal
  • Show empty, loading, and error states
  • Keep row actions consistent across rows

Don't

  • Load huge datasets into the browser unnecessarily
  • Show too many columns on mobile
  • Hide search for large datasets
  • Run destructive bulk actions without confirmation
  • Reset filters unexpectedly
  • Rely only on colour for status indicators
  • Use raw colours outside Byteion semantic tokens
  • Use virtual scroll when normal server pagination is simpler

Developer tips

• Keep query state close to table state — search, filters, sort, and page are tightly coupled and should live together. • Reset page to 1 when filters or search change — paginating into a filtered subset is confusing. • Debounce backend search — wait 300–400ms after the user stops typing before sending the request. • Preserve state in URL query for shareable admin screens (/customers?search=olivia&page=2&sort=name&order=asc). • Coordinate table state with the API client, router, loading state, and toast system. • Store user preferences for column visibility, rows per page, and sort order when helpful (localStorage or user settings API). • In Laravel apps, use query params for search/filter/sort/page and return paginated results from the backend. Use request()->query() to read params and Model::paginate() for server-side pagination. • In React/Vue apps, keep pagination and filters in state or route query. Use DataTable controlled props for full state management. • Test with empty data, large data, long text, slow API responses, failed API responses, and permission-restricted users.

Production checklist

Layout

Toolbar visible

Search and filters are above the table

Result count shown

"Showing 1–10 of 32" is visible

Bulk action bar

Appears only when rows are selected

Pagination works

Page navigation, prev/next, rows-per-page tested

Accessibility

Semantic table

<table>, <thead>, <th scope="col"> used

aria-sort used

Active sort column has aria-sort

Row checkboxes labelled

aria-label="Select {name}" on each checkbox

Pagination nav

<nav aria-label="Pagination"> wrapping controls

Status text visible

Badges have text labels, not colour alone

Focus predictable

Focus does not jump unexpectedly after actions

Data loading

Loading state

Skeleton shown while fetching

Empty state

Empty message shown when no records exist

Error state

Error message + retry on fetch failure

Server pagination

Backend paginates, not the browser, for large datasets

Search works

Results update as user types (debounced)

Filters reset

Page resets to 1 when filters change

Sort preserved

Sort direction is maintained across pages

Filtered empty

Different message from fully empty state

Bulk actions

Bar visible on select

Bulk bar appears only when rows selected

Destructive confirmed

Modal confirmation before bulk delete/archive

Loading during action

Buttons disabled + spinner during processing

Selection cleared

Selection resets after successful bulk action

Partial failure

Clear feedback if some items fail

Permissions / Security

Backend enforcement

Server validates permission for every action

Sensitive data masked

PII and secrets are not exposed unnecessarily

Mobile

Horizontal scroll

Table scrolls inside container; page does not overflow

Few columns visible

5–6 columns max; secondary columns hidden

Bulk actions accessible

Bulk bar full-width and tappable

Theme testing

Dark mode

All surfaces, borders, badges work with .dark

Branded theme

Primary palette works across badges, buttons, and select states

Backend integration

Pagination params

Backend accepts page, perPage, search, sort, filter params

Audit logging

Bulk/row destructive actions logged server-side

  • Table — semantic table primitive with compound sub-components
  • DataTable — opinionated data grid with built-in search, sort, pagination, selection
  • Input — search field and form inputs
  • Select — filter and rows-per-page dropdowns
  • Button — action buttons with loading state
  • Badge — status indicators with text labels
  • Checkbox — row selection and select-all
  • Pagination — page navigation controls
  • Dropdown / Menu — row action menus
  • Alert — error and success messages
  • Empty State — no-data display
  • Skeleton / Loading — loading placeholders
  • Modal / Dialog — destructive confirmation
  • Toast — success and error notifications

Unlock Premium Patterns

Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.

Explore Pro Components