Data Table

New

Server-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.

Name Role Joined Actions
Alice Martin Admin 2024-01-15 Edit
Bob Chen Editor 2024-03-22 Edit
Carol Davis Viewer 2024-06-10 Edit
<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

Extra small (xs)

ID Name Amount
1001 Widget Alpha $120.00
1002 Widget Beta $85.50
<c-data-table size="xs" zebra>...</c-data-table>

Large (lg)

ID Name Amount
1001 Widget Alpha $120.00
1002 Widget Beta $85.50
<c-data-table size="lg">...</c-data-table>

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'));