DataTable
The DataTable component is the primary way data is displayed in the admin panel. It supports server-side pagination, column sorting, filtering, search, custom cell renderers, row actions, and data export — all driven by your resource definition.
Server-Side Pagination
The DataTable never loads the entire dataset into memory. It communicates with your Go API using query parameters for page, page size, sort, and filter values. The API returns a paginated response with a meta object containing total count, current page, page size, and total pages.
Pagination controls appear at the bottom of the table showing the current range (e.g. "Showing 1-20 of 156"), page navigation buttons, and a page size selector (10, 20, 50, 100 rows).
GET /api/posts?page=1&page_size=20&sort=created_at&order=desc&search=hello
Response:
{
"data": [ ... ],
"meta": {
"total": 156,
"page": 1,
"page_size": 20,
"pages": 8
}
}The default page size is controlled by table.pageSize in your resource definition (default: 20). Users can change the page size at runtime using the selector in the pagination bar.
Column Sorting
Any column with sortable: true in its definition becomes clickable. Clicking a column header cycles through three states:
- Ascending — small arrow pointing up appears next to the header.
- Descending — arrow points down.
- No sort — returns to the default sort order.
Sorting sends sort and order query parameters to the API. Only single-column sorting is supported (clicking a new column clears the previous sort). You can set a default sort in the resource definition:
table: {
defaultSort: { key: 'created_at', direction: 'desc' },
columns: [ ... ],
}Column Filtering
Filters appear as a horizontal bar above the data table. Three filter types are supported, each rendering a different control:
Select Filter
Renders a dropdown with predefined options. Useful for status fields, categories, or any column with a fixed set of values.
{ key: 'status', type: 'select', options: ['active', 'inactive', 'banned'] }Date Range Filter
Renders two date pickers (from and to) for filtering records within a time window. Sends created_at_from and created_at_to query parameters.
{ key: 'created_at', type: 'date-range', label: 'Created Date' }Number Range Filter
Renders two number inputs (min and max) for filtering numeric values. Useful for price ranges, quantities, or scores. Sends amount_min andamount_max query parameters.
{ key: 'amount', type: 'number-range', label: 'Amount' }Active filters show a count badge on the filter bar and a "Clear filters" button appears when any filter is applied. Changing filters resets the page to 1.
Show/Hide Columns
A column visibility dropdown appears in the table toolbar. Users can toggle individual columns on or off. Columns with hidden: true in their definition are hidden by default but can be shown via the dropdown. Column visibility preferences are persisted in localStorage so they survive page reloads.
Search
When table.searchable is true (the default), a search input appears in the table toolbar. Typing into it sends a search query parameter to the API. On the Go side, the search handler applies aILIKE query across all columns marked with searchable: true in the resource definition.
Search is debounced at 300ms to avoid excessive API calls while the user is typing.
Custom Cell Renderers
The format and badge column options cover most use cases. Here are examples of each renderer:
columns: [
// Badge — colored pills for status values
{ key: 'status', label: 'Status', badge: {
active: { color: 'green', label: 'Active' },
inactive: { color: 'gray', label: 'Inactive' },
}},
// Currency — formatted as $1,234.50
{ key: 'amount', label: 'Amount', format: 'currency' },
// Date — formatted as "Jan 15, 2026"
{ key: 'due_date', label: 'Due Date', format: 'date' },
// Relative — formatted as "3 hours ago"
{ key: 'created_at', label: 'Created', format: 'relative' },
// Boolean — green checkmark or red X
{ key: 'active', label: 'Active', format: 'boolean' },
// Image — 32x32 rounded thumbnail
{ key: 'avatar', label: 'Avatar', format: 'image' },
// Number — formatted with thousand separators
{ key: 'views', label: 'Views', format: 'number' },
// Relation — displays a field from a related object
{ key: 'customer.name', label: 'Customer', relation: 'customer' },
// Video — thumbnail with play overlay
{ key: 'preview', label: 'Preview', format: 'video' },
// Link — clickable URL with hostname
{ key: 'website', label: 'Website', format: 'link' },
// Email — clickable mailto link
{ key: 'email', label: 'Email', format: 'email' },
// Color — swatch circle with hex value
{ key: 'color', label: 'Color', format: 'color' },
]Column Styling
Add the className property to any column definition to apply custom Tailwind CSS classes to every cell in that column. This wraps the rendered content in a <span> with your classes, so it works alongside any format type.
columns: [
// Bold title column
{ key: 'title', label: 'Title', className: 'font-semibold text-foreground' },
// Green currency column
{ key: 'price', label: 'Price', format: 'currency', className: 'text-success' },
// Monospace code column
{ key: 'sku', label: 'SKU', className: 'font-mono text-xs tracking-wider' },
// Truncated long text
{ key: 'description', label: 'Description', className: 'max-w-[200px] truncate' },
]Row Actions
Each row has an actions menu (three-dot icon) on the right side. The available actions are controlled by the table.actions array in your resource definition:
- create — adds a "New [Resource]" button to the table toolbar that opens the create form modal.
- edit — opens the edit form modal pre-filled with the row data.
- delete — shows a confirmation dialog, then sends a DELETE request to the API.
- view — navigates to a detail page for the resource.
- export — adds CSV/JSON export buttons to the toolbar.
Delete actions use optimistic updates via React Query — the row is removed from the table immediately and restored if the API call fails.
table: {
actions: ['create', 'edit', 'delete', 'export'],
bulkActions: ['delete', 'export'],
columns: [ ... ],
}Bulk Actions
When bulkActions are defined, each row gets a checkbox on the left side. Selecting one or more rows reveals a floating action bar at the bottom of the table with the configured bulk actions. For example, selecting 5 rows and clicking "Delete" sends 5 DELETE requests in parallel.
Empty State
When a resource has no data (or no results match the current filters), the DataTable shows a polished empty state with:
- An illustration matching the resource icon
- A message like "No posts yet"
- A "Create your first post" button that opens the create form modal
The empty state maintains the table's full width and height so the layout does not collapse.
Loading Skeleton
While data is being fetched, the DataTable renders a skeleton loader that matches the exact layout of the table — header row, column widths, and row heights are preserved. This prevents layout shift when data loads and gives users confidence that content is on its way.
Subsequent page navigations (changing page, applying filters) show a subtle loading indicator in the table header instead of replacing the entire table with a skeleton. This keeps the current data visible while the new data loads.
Export to CSV / JSON
When 'export' is included in table.actions, export buttons appear in the table toolbar. Users can export the current filtered and sorted view as CSV or JSON. The export fetches all matching records from the API (not just the current page) and triggers a browser download.
Column labels are used as CSV headers. Hidden columns are excluded from the export unless explicitly shown. Badge values export as their raw value (e.g."paid") rather than the display label.
Responsive Behavior
On screens narrower than the table's natural width, the DataTable enables horizontal scrolling. The row actions column is sticky on the right side so it remains visible while scrolling. On mobile devices, the filter bar collapses into a "Filters" button that opens a sheet overlay.
Full Table Configuration Example
table: {
columns: [
{ key: 'id', label: 'ID', sortable: true, hidden: true },
{ key: 'number', label: 'Invoice #', sortable: true, searchable: true },
{ key: 'customer.name', label: 'Customer', relation: 'customer',
searchable: true },
{ key: 'amount', label: 'Amount', format: 'currency', sortable: true },
{ key: 'status', label: 'Status', badge: {
paid: { color: 'green', label: 'Paid' },
pending: { color: 'yellow', label: 'Pending' },
overdue: { color: 'red', label: 'Overdue' },
}},
{ key: 'due_date', label: 'Due Date', format: 'date', sortable: true },
{ key: 'created_at', label: 'Created', format: 'relative' },
],
filters: [
{ key: 'status', type: 'select',
options: ['paid', 'pending', 'overdue'] },
{ key: 'created_at', type: 'date-range' },
{ key: 'amount', type: 'number-range' },
],
pageSize: 20,
defaultSort: { key: 'created_at', direction: 'desc' },
searchable: true,
actions: ['create', 'edit', 'delete', 'view', 'export'],
bulkActions: ['delete', 'export', 'mark-paid'],
}