Data Table
NewServer-driven sortable data table built on HTMX. All sort and filter state lives on the server. Column headers render as sort links; HTMX replaces the table fragment on each interaction.
API Reference
No client-side state. The current sort column, sort direction, active filters, and pagination offset are all query parameters on the URL. The server reads these params, queries the database, and returns the rendered table HTML fragment.
HTMX intercepts sort-link clicks and filter-input changes, fires a GET request to url, and swaps the response into target (the table wrapper div). Because the full fragment is returned, the server can also update pagination controls, row counts, and any other summary information in the same response.
c-data-table
| Attribute | Description | Type | Default |
|---|---|---|---|
url
|
HTMX GET endpoint that returns the refreshed table fragment | string |
""
|
target
|
CSS selector for the HTMX swap target. When empty, the table swaps itself. | string |
""
|
size
|
Table density variant (mirrors c-table)
|
string |
"md"
|
zebra
|
Zebra striping on rows | boolean |
False
|
sort_param
|
Query parameter name for the sort column key | string |
"sort"
|
order_param
|
Query parameter name for sort direction (asc / desc)
|
string |
"order"
|
current_sort
|
Currently active sort column key (from server context) | string |
""
|
current_order
|
Current sort direction — asc or desc
|
string |
"asc"
|
loading
|
When True, overlays a spinner on the table body
|
boolean |
False
|
c-data-table.header
| Attribute | Description | Type | Default |
|---|---|---|---|
sort_key
|
The field/column key sent as the sort_param value. When empty, the column is not sortable and renders plain text.
|
string |
""
|
url
|
Base URL for the sort link (must match c-data-table's url)
|
string |
""
|
sort_param
|
Query parameter name for sort column (should match parent table's setting) | string |
"sort"
|
order_param
|
Query parameter name for sort direction | string |
"order"
|
current_sort
|
Active sort column (pass down from parent or server context) | string |
""
|
current_order
|
Active sort direction | string |
"asc"
|
size
xs
sm
md
lg
xl
Accessibility
Sort indicators: sortable column headers render as anchor links with aria-sort attribute set to ascending or descending when active. Non-sortable columns render as plain <th> elements.
Loading state: when :loading=True, a spinner overlay is shown. Consider also updating an aria-live region to announce loading state changes to screen reader users.
Keyboard navigation: sort links are standard anchors and receive natural tab order. HTMX requests triggered by sort links update only the table fragment, preserving the user's focus position on the page.
Examples
Usage
<c-data-table url="/api/users/" current_sort="" current_order="" zebra>
<thead><tr>
<c-data-table.header sort_key="name" url="/api/users/" current_sort="" current_order="">Name</c-data-table.header>
</tr></thead>
<tbody>{% for user in users %}<tr><td>{{ user.name }}</td></tr>{% endfor %}</tbody>
</c-data-table>
Basic Sortable Table
This example uses static data to show the HTML structure. In production the server renders the fragment with the correct current_sort and current_order values so the active column indicator updates on each request.
<c-data-table
url="/api/users/"
current_sort=""
current_order=""
zebra
>
<thead>
<tr>
<c-data-table.header
sort_key="name"
url="/api/users/"
current_sort=""
current_order=""
>Name</c-data-table.header>
<c-data-table.header
sort_key="role"
url="/api/users/"
current_sort=""
current_order=""
>Role</c-data-table.header>
<c-data-table.header>Actions</c-data-table.header>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.role }}</td>
</tr>
{% endfor %}
</tbody>
</c-data-table>
Loading State
Pass :loading=True to overlay a spinner on the table. In practice this is set from the server context when the view detects a slow query or an in-progress background task.
| Name | Status | Date |
|---|---|---|
<c-data-table :loading=True>
...
</c-data-table>
context["loading"] = not results_ready
Size Variants
Django View Integration
The view reads sort and order query params, applies order_by to the queryset, and passes them back to the template. HTMX requests (detected via the HX-Request header) return only the table fragment; full-page requests render the complete layout.
# views.py
from django.views.generic import ListView
from .models import User
SORTABLE_FIELDS = {"name", "role", "joined"}
class UserListView(ListView):
model = User
template_name = "users/list.html"
paginate_by = 25
def get_queryset(self):
qs = super().get_queryset()
sort = self.request.GET.get("sort", "name")
order = self.request.GET.get("order", "asc")
if sort in SORTABLE_FIELDS:
prefix = "-" if order == "desc" else ""
qs = qs.order_by(f"{prefix}{sort}")
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["current_sort"] = self.request.GET.get("sort", "name")
ctx["current_order"] = self.request.GET.get("order", "asc")
ctx["table_url"] = self.request.path
return ctx
def get_template_names(self):
if self.request.headers.get("HX-Request"):
return ["users/partials/table.html"]
return [self.template_name]
Manual Refresh via Custom Event
When url is set, the table also listens for the kaiko-data-table-refresh window event. Dispatch it from anywhere on the page to force a data reload — for example, after a form submission that creates a new row.
<!-- After a successful HTMX form POST, trigger the table to refresh: -->
<form
hx-post="/api/users/create/"
hx-on::after-request="window.dispatchEvent(new Event('kaiko-data-table-refresh'))"
>
...
</form>
<!-- Or from plain JavaScript: -->
window.dispatchEvent(new Event('kaiko-data-table-refresh'));