Working with Components¶
This guide covers using the Cotton components provided by django-htmx-plus.
Overview¶
django-htmx-plus provides Cotton components for:
Rendering sortable table headers
Displaying paginated tables with Bootstrap 5 styling
Building complete interactive tables with minimal template code
All components work with the context variables provided by HtmxListView.
Complete Table Component¶
The simplest way to build a table is using the complete component:
{% extends "base.html" %}
{% load cotton_extras %}
{% block content %}
<div id="article-table" hx-trigger="articleCreated from:body">
<c-tables.htmx_table class="table table-striped table-hover" />
</div>
{% endblock %}
This single component renders:
Table headers with sorting
Table rows
Pagination controls
All automatic from your HtmxListView context.
Custom Table Structure¶
For more control, build your table manually with individual components:
{% load cotton_extras %}
<table class="table table-striped">
<c-tables.head>
<c-tables.header_cell name="title">Title</c-tables.header_cell>
<c-tables.header_cell name="status">Status</c-tables.header_cell>
<c-tables.header_cell name="created_at">Date</c-tables.header_cell>
<th>Actions</th>
</c-tables.head>
<tbody>
{% for article in objects %}
<tr>
<td>{{ article.title }}</td>
<td>{{ article.status }}</td>
<td>{{ article.created_at|date:"Y-m-d" }}</td>
<td>
<a href="{% url 'article-edit' article.id %}" class="btn btn-sm btn-primary">Edit</a>
<a href="{% url 'article-delete' article.id %}" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<c-tables.pager />
Sortable Headers¶
Make column headers sortable:
<c-tables.head>
<c-tables.header_cell name="title">Title</c-tables.header_cell>
<c-tables.header_cell name="author">Author</c-tables.header_cell>
<c-tables.header_cell name="created_at">Date</c-tables.header_cell>
</c-tables.head>
Each header:
Shows a chevron icon indicating sort direction when active
Links to toggle sort direction (ascending → descending → no sort)
Uses HTMX to update the table without page reload
The order_by context variable shows the current sort field.
Pagination¶
Add pagination controls with a single component:
<c-tables.pager />
Features:
Previous/Next buttons (disabled at boundaries)
Elided page numbers (shows first, last, and pages around current)
All links use HTMX for smooth updates
Bootstrap 5 pagination styling
Example with page range display:
<nav aria-label="Page navigation">
<c-tables.pager />
</nav>
Icon Components¶
Use icon components directly for custom layouts:
{% load cotton_extras %}
<!-- Chevron up (ascending sort) -->
<c-icons.chevron_up />
<!-- Chevron down (descending sort) -->
<c-icons.chevron_down />
<!-- Chevron left (previous page) -->
<c-icons.chevron_left />
<!-- Chevron right (next page) -->
<c-icons.chevron_right />
All icons accept an optional add_class attribute:
<c-icons.chevron_up add_class="ms-1" />
Advanced: Custom Styling¶
Apply custom CSS classes to the table:
<c-tables.htmx_table class="table table-striped table-hover table-sm" />
Use Bootstrap table utilities:
table-striped– Alternating row colorstable-hover– Highlight row on hovertable-sm– Smaller tabletable-bordered– Borders on all cellstable-dark– Dark theme
Advanced: Multiple Tables on Same Page¶
Each table needs a unique target_id:
class DraftArticleListView(HtmxListView):
model = Article
template_name = "article/drafts.html"
target_id = "#draft-table"
def get_queryset(self):
return super().get_queryset().filter(status="draft")
class PublishedArticleListView(HtmxListView):
model = Article
template_name = "article/published.html"
target_id = "#published-table"
def get_queryset(self):
return super().get_queryset().filter(status="published")
Template with both tables:
{% extends "base.html" %}
{% load cotton_extras %}
{% block content %}
<div class="container mt-4">
<h2>Draft Articles</h2>
<div id="draft-table" hx-trigger="articleCreated,articleUpdated from:body">
<c-tables.htmx_table class="table table-striped" />
</div>
<h2>Published Articles</h2>
<div id="published-table" hx-trigger="articleCreated,articleUpdated from:body">
<c-tables.htmx_table class="table table-striped" />
</div>
</div>
{% endblock %}
Each table updates independently based on filters and sorting.
Integration with Modals¶
Refresh a table when items are created in a modal:
{% extends "base.html" %}
{% load django_htmx_plus %}
{% load cotton_extras %}
{% block modal %}
<div class="modal fade" data-htmx-plus-modal="article-form">
<div class="modal-dialog">
<div id="article-form" class="modal-content"></div>
</div>
</div>
{% endblock %}
{% block content %}
<button hx-get="{% url 'article-create' %}" hx-target="#article-form" class="btn btn-primary">
New Article
</button>
<div id="article-table" hx-trigger="articleCreated from:body">
<c-tables.htmx_table class="table table-striped" />
</div>
{% endblock %}
When the form fires the articleCreated trigger, the table automatically refreshes.
Best Practices¶
Use complete table component for simple cases – Saves boilerplate
Build custom tables for complex layouts – Maximum flexibility
Set unique target_id per table – Ensures correct HTMX targeting
Use meaningful sort fields – Not all fields need sorting
Add HTMX triggers for automatic updates – Keeps UI in sync
Test sorting and pagination – Ensure query string handling works
Consider performance – Pagination helps with large datasets