Tables Component¶
Version: 0.6.0
Last Updated: January 3, 2026
Location: src/templates/partials/tables.html
Tables display structured data in rows and columns. Arctyk tables integrate Bootstrap 5 styling, sorting capabilities, responsive design, and accessibility features.
Overview¶
Table features:
- Bootstrap 5 responsive design
- Sortable columns with visual indicators
- Pagination support
- Hover effects and row selection
- Icon support with Bootstrap Icons
- Status indicators and badges
- Responsive table-scroll on mobile
- Keyboard accessible
- ARIA compliant
Basic Table¶
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Title</th>
<th>Status</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% for ticket in tickets %}
<tr>
<td>#{{ ticket.ticket_number }}</td>
<td>
<a href="{% url 'tickets:ticket_detail' ticket.pk %}">
{{ ticket.title }}
</a>
</td>
<td>
<span class="badge bg-{{ ticket.status_color }}">
{{ ticket.get_status_display }}
</span>
</td>
<td>{{ ticket.created_at|date:"M d, Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Sortable Tables¶
With Sort Headers¶
{% load sort_tags %}
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>{% sort_header 'ticket_number' 'ID' %}</th>
<th>{% sort_header 'title' 'Title' %}</th>
<th>{% sort_header 'status' 'Status' %}</th>
<th>{% sort_header 'created_at' 'Created' %}</th>
</tr>
</thead>
<tbody>
{% for ticket in tickets %}
<tr>
<td>#{{ ticket.ticket_number }}</td>
<td>{{ ticket.title }}</td>
<td>
<span class="badge bg-{{ ticket.status_color }}">
{{ ticket.get_status_display }}
</span>
</td>
<td>{{ ticket.created_at|date:"M d, Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Styled Variants¶
Striped Rows¶
Bordered Table¶
Small Table¶
Row States¶
Clickable Rows¶
<tbody>
{% for ticket in tickets %}
<tr
class="table-row"
data-href="{% url 'tickets:ticket_detail' ticket.pk %}"
tabindex="0"
role="button"
>
<td>#{{ ticket.ticket_number }}</td>
<td>{{ ticket.title }}</td>
<td><span class="badge">{{ ticket.status }}</span></td>
</tr>
{% endfor %}
</tbody>
<script>
document.querySelectorAll(".table-row").forEach((row) => {
row.addEventListener("click", function () {
window.location = this.dataset.href;
});
row.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
window.location = this.dataset.href;
}
});
});
</script>
Row Highlighting¶
<tbody>
{% for ticket in tickets %}
<tr
class="{% if ticket.is_urgent %}table-danger{% elif ticket.is_overdue %}table-warning{% endif %}"
>
<td>{{ ticket.title }}</td>
</tr>
{% endfor %}
</tbody>
Complex Columns¶
Actions Column¶
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for ticket in tickets %}
<tr>
<td>{{ ticket.title }}</td>
<td><span class="badge">{{ ticket.status }}</span></td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a
href="{% url 'tickets:ticket_detail' ticket.pk %}"
class="btn btn-outline-primary"
title="View"
>
<i class="bi bi-eye"></i>
</a>
<a
href="{% url 'tickets:ticket_update' ticket.pk %}"
class="btn btn-outline-secondary"
title="Edit"
>
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
Status with Icon¶
<td>
<i
class="bi bi-{{ ticket.status_icon }}"
class="me-2"
title="{{ ticket.get_status_display }}"
></i>
<span>{{ ticket.get_status_display }}</span>
</td>
Responsive Design¶
Responsive Wrapper¶
Mobile Column Labels¶
<table class="table" data-mobile-labels>
<thead class="d-none d-md-table-header-group">
<tr>
<th>Title</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for ticket in tickets %}
<tr class="d-flex flex-column d-md-table-row">
<td data-label="Title">{{ ticket.title }}</td>
<td data-label="Status">{{ ticket.status }}</td>
<td data-label="Date">{{ ticket.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Empty State¶
{% if not tickets %}
<div class="alert alert-info mt-4">
<i class="bi bi-info-circle me-2"></i>
<strong>No tickets found.</strong>
<a href="{% url 'tickets:ticket_create' %}">Create one now</a>
</div>
{% else %}
<table class="table">
<!-- Table content -->
</table>
{% endif %}
View Implementation¶
from django.views.generic import ListView
from tickets.models import Ticket
class TicketListView(ListView):
model = Ticket
template_name = 'tickets/ticket_list.html'
context_object_name = 'tickets'
paginate_by = 20
def get_queryset(self):
queryset = Ticket.objects.all().select_related('assigned_to')
# Sorting
sort = self.request.GET.get('sort', '-created_at')
if sort in ['title', '-title', 'created_at', '-created_at']:
queryset = queryset.order_by(sort)
# Filtering
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['sort'] = self.request.GET.get('sort')
context['status_filter'] = self.request.GET.get('status')
return context
Accessibility¶
ARIA Attributes¶
<table class="table" role="grid" aria-label="Ticket list">
<thead role="rowgroup">
<tr role="row">
<th role="columnheader">Title</th>
<th role="columnheader">Status</th>
<th role="columnheader">Date</th>
</tr>
</thead>
<tbody role="rowgroup">
{% for ticket in tickets %}
<tr role="row">
<td role="gridcell">{{ ticket.title }}</td>
<td role="gridcell">{{ ticket.status }}</td>
<td role="gridcell">{{ ticket.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Caption¶
<table class="table">
<caption>
List of support tickets
</caption>
<thead>
<tr>
<th>Title</th>
<th>Status</th>
</tr>
</thead>
</table>
Testing¶
Table Rendering Tests¶
from django.test import TestCase, Client
from tickets.models import Ticket
class TicketTableTest(TestCase):
@classmethod
def setUpTestData(cls):
for i in range(5):
Ticket.objects.create(title=f'Ticket {i}')
def test_table_renders(self):
response = self.client.get('/tickets/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<table')
def test_table_rows_count(self):
response = self.client.get('/tickets/')
self.assertEqual(response.context['tickets'].count(), 5)
def test_table_sorting(self):
response = self.client.get('/tickets/?sort=-title')
tickets = list(response.context['tickets'])
self.assertEqual(tickets[0].title, 'Ticket 4')
def test_empty_state(self):
Ticket.objects.all().delete()
response = self.client.get('/tickets/')
self.assertContains(response, 'No tickets found')
CSS Customization¶
// Custom table styling
.table {
font-size: 0.95rem;
thead {
background-color: #f8f9fa;
font-weight: 600;
}
tbody tr {
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
}
}
td {
vertical-align: middle;
padding: 1rem 0.75rem;
}
.badge {
white-space: nowrap;
}
}
// Responsive
@media (max-width: 768px) {
.table {
font-size: 0.85rem;
td {
padding: 0.75rem 0.5rem;
}
}
}
Best Practices¶
✅ Use semantic HTML (<table>, <thead>, <tbody>, <th>)
✅ Always include column headers
✅ Use responsive wrapper for mobile
✅ Show empty state when no data
✅ Include sorting/filtering capabilities
✅ Use badges for status indicators
✅ Make rows clickable with proper keyboard support
✅ Include pagination for large datasets
❌ Don't use tables for layout
❌ Don't nest tables
❌ Don't truncate important data without tooltips
❌ Don't use horizontal scrolling on mobile