Horilla CRM uses HTMX for partial page updates while keeping Django server-rendered templates. The shell loads once; #mainContent and #modalBox swap HTML fragments on each interaction.
This post is Part 5 of 28 in the Horilla CRM Technical Blog series.
What is HTMX in Horilla CRM?
HTMX in Horilla CRM enables you to:
- MAIN_CONTENT_HX_ATTRS for sidebar navigation
- List → detail transitions without full reload
- Modals for create/edit via #modalBox
- htmx_required on fragment-only endpoints
- OOB swaps for settings sidebar highlights
The page shell: stable regions
The main layout defines persistent DOM targets:
| Target ID | Purpose |
|---|---|
| #mainContent | Primary work area (list, detail, settings pages) |
| #modalBox | Create/edit modals |
| #settings-content | Settings module inner panel |
| #settings-sidebar | Settings navigation (OOB swaps) |
The shell loads once; inner regions swap via HTMX.
Pattern 1: In-app navigation with MAIN_CONTENT_HX_ATTRS
From horilla/menu/__init__.py:
MAIN_CONTENT_HX_ATTRS = {
"hx-boost": "true",
"hx-target": "#mainContent",
"hx-select": "#mainContent",
"hx-swap": "outerHTML",
}
Apply to sidebar links:
# menu.py sub-section item
{
"label": _("Leads"),
"url": reverse_lazy("leads:leads_view"),
**MAIN_CONTENT_HX_ATTRS,
"perm": "leads.view_lead",
}
What happens: Clicking “Leads” fetches the full HTML response, extracts #mainContent from it, and replaces the current main region. The sidebar/header stays mounted — no full reload.
hx-push-url=”true” on detail links updates the browser URL for bookmarking.
Pattern 2: List → detail without leaving the shell
From LeadListView.col_attrs:
"title": {
"hx-get": "{get_detail_url}",
"hx-target": "#mainContent",
"hx-swap": "outerHTML",
"hx-push-url": "true",
"hx-select": "#mainContent",
"permission": "leads.view_lead",
"own_permission": "leads.view_own_lead",
"owner_field": "lead_owner",
}
{get_detail_url} resolves to the model’s get_detail_url() method. Generic list templates expand these placeholders per row.
Pattern 3: Modals for create/edit
Floating menu create actions target the modal:
@floating_menu.register
class LeadFloating:
items = {
"hx-target": "#modalBox",
"hx-swap": "innerHTML",
"onclick": "openModal()",
"perm": ["leads.add_lead"],
}
Form views return partial templates that fit inside #modalBox. On success, HTMX triggers may refresh #mainContent or close the modal via HX-Trigger headers.
Pattern 4: htmx_required on partial endpoints
Many list/kanban/nav views require HTMX so they cannot be bookmarked as bare HTML fragments:
from horilla.utils.decorators import htmx_required
@method_decorator(htmx_required, name="dispatch")
class LeadListView(LoginRequiredMixin, HorillaListView):
...
Direct browser navigation to /crm/leads/leads-list/ may redirect or 400; users enter through the shell view (leads-view/) which loads chrome then HX-gets the list.
Why: Prevents broken layouts when users open fragment URLs in new tabs without the shell (some views allow fallback — check per module).
Pattern 5: Settings with out-of-band (OOB) swaps
Settings menu items often update two regions at once:
{
"label": _("Assignment Rules"),
"url": reverse_lazy("leads:leads_assignment_view"),
"hx-target": "#settings-content",
"hx-select": "#lead-assignment-view",
"hx-select-oob": "#settings-sidebar",
"perm": "leads.view_leadassignmentrule",
}
hx-select-oob replaces #settings-sidebar from the same response — highlight active section without a second request.
Pattern 6: Activity tab — nested HTMX
The activity module (horilla/contrib/activity/templates/activity_tab.html) uses HTMX for:
- Filtering activities by type
- Inline task completion
- Loading note/meeting forms into sub-panels
Typical attributes you’ll see:
<button hx-get="{% url 'activity:task_toggle' pk=task.id %}"
hx-target="#activity-list-{{ object.pk }}"
hx-swap="innerHTML">
Detail record tabs load activity via hx-get on tab click — deferring heavy queries until the user opens the tab.
Pattern 7: View mode switching (list/kanban/card)
The nav bar (HorillaNavView) renders toggle buttons that HX-get different endpoints into #mainContent:
leads-list/ → HorillaListView
leads-kanban/ → HorillaKanbanView
leads-card/ → HorillaCardView
Shared filterset_class keeps filter state consistent across modes (query string preserved).
HTMX + permissions
Template tags and generic mixins strip hx-* attributes when the user lacks permission — buttons disappear rather than failing at click time.
Always pair HTMX endpoints with the same decorators as full-page views:
@permission_required_or_denied(["leads.view_lead", "leads.view_own_lead"])
HTMX + real-time (Channels)
Notifications use WebSockets (horilla_notifications) alongside HTMX — not instead of it.
Typical flow:
- Server event → push notification JSON over WebSocket
- Client JS updates badge/toast
- User clicks → HTMX loads detail into #mainContent
Keep domain logic on the server; use WS for ephemeral alerts.
Frontend stack (no SPA)
| Layer | Choice |
|---|---|
| CSS | Tailwind + Flowbite |
| Charts | ECharts |
| Rich text | Summernote |
| Select FK | Select2 AJAX (/generics/…/select2/) |
| Interactivity | HTMX + small vanilla JS helpers |
AppLauncher registers per-app JS via js_files (Part 2) for module-specific behavior.
Debugging tips
- Network tab — HTMX requests include HX-Request: true header
- Wrong fragment — check hx-select matches an ID in the response template
- Full page in #mainContent — view template may extend wrong base; partials should extend a minimal base or use {% block %} only
- 403 on HX-get — permission decorator or missing CSRF on POST
- Stale sidebar — add hx-select-oob for secondary regions
Benefits of HTMX in Horilla CRM
- SPA-like speed without a JavaScript framework
- Easier debugging — HTML over the wire
- Smaller frontend bundles
- Permissions strip hx-* attrs for unauthorized users
- Works with WebSocket notifications for alerts
HTMX is central to Horilla CRM’s UX. Master #mainContent, modal targets, and htmx_required — then every module feels fast and consistent.
Continue the series
Previous: Part 4 — How Horilla CRM’s Four-Layer Permission System Enhances Data Security
Next: Part 6 — How to Extend Horilla CRM Without Forking the Core Framework
More posts are at Horilla Blogs; share feedback on GitHub.