A CRM list page usually has several things above the records. Users may need to search for records, apply filters, switch between saved views, change the layout, reload the page, or create a new record. These controls also need to work properly with permissions and htmx.
If every module builds these elements separately, the same HTML and JavaScript-related setup ends up being repeated across Leads, Contacts, Opportunities, Campaigns, and other modules.
Horilla provides HorillaNavView to avoid that repetition. It gives you a common navbar that can handle these features through configuration instead of requiring you to build the navbar HTML for every page.
The base class is available in:
horilla/contrib/generics/views/navbar.py
and the navbar uses:
horilla/contrib/generics/templates/navbar.html
The class is built on Django’s TemplateView, so you can extend it in the same way as other generic views.
Let’s start with a simple example and then look at the options available when you need more control.
1. Creating a Basic Navbar
The basic setup only needs a few attributes. For example, a Contacts navbar can be created like this:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from horilla.contrib.generics.views import HorillaNavView
from .filters import ContactFilter
class ContactNavbar(LoginRequiredMixin, HorillaNavView):
search_url = reverse_lazy("contacts:contact_list_view")
main_url = reverse_lazy("contacts:contacts_view")
model_name = "Contact"
model_app_label = "contacts"
filterset_class = ContactFilter
With this configuration, Horilla can build the basic navbar for the Contacts page.
The title can be resolved from the model, while the navbar can also provide options such as the view-type dropdown, search field, and filter button.
The important point here is that you don’t have to create the navbar markup yourself. You tell HorillaNavView which model, URLs, and filter set the page uses, and the generic view takes care of the common parts.
2. Understanding the Configuration Options
HorillaNavView has a number of class attributes that control different parts of the navbar.
You don’t need to configure all of them. Usually, you only set the options required by the particular page.
Identity and Routing
These attributes identify the model and define the URLs used by the navbar.
| Variable | Default | Purpose |
| model_name | “” | Model name used for resolving the title and permissions. |
| model_app_label | “” | App label associated with the model. |
| nav_title | Resolved from model | Allows you to provide a custom navbar title. |
| nav_description | “” | Optional description displayed below the title. |
| search_url | “” | URL used for search, filters, and view-related requests. |
| main_url | “” | URL for the main list page. |
| main_session_id | “mainSession” | ID of the htmx content target. |
For example, if the navbar is being used in a different section of the page and the default session ID could cause a conflict, you can change it:
main_session_id = “leadAssignmentSession”
This is particularly useful when a navbar appears inside a modal, tab, or another area where the default #mainSession could overlap with another content area.
View Types and Saved Views
The navbar can also provide different record views and saved lists.
| Variable | Default | Purpose |
| all_view_types | True | Displays the view-type dropdown. |
| recently_viewed_option | True | Adds the Recently Viewed option. |
| custom_view_type | {} | Allows module-specific view types. |
| save_to_list_option | Not False | Shows the option to save the current filters as a list. |
| one_view_only | False | Keeps the navbar on one view and hides the layout selector. |
If a module needs a custom view, you can define one using a @cached_property.
For example:
from django.utils.functional import cached_property
@cached_property
def custom_view_type(self):
return {
"converted_lead": {
"name": _("Converted Lead"),
"show_list_only": True,
},
}
Here, show_list_only tells the navbar that the Converted Lead view should remain a list view. The other layout options are not shown for that view.
Layouts
One of the useful parts of HorillaNavView is that the navbar can provide different layouts for the same records.
| Variable | Default | Purpose |
| kanban_url | “” | Enables the Kanban layout. |
| card_url | “” | Enables the Card layout. |
| group_by_url | “” | Enables the Group By layout. |
| timeline_url | “” | Enables the Timeline layout. |
| chart_url | “” | Enables the Chart View layout. |
| split_view_url | “” | Enables the Split View layout. |
| default_layout | “list” | Layout used when no ?layout= is provided. |
| exclude_kanban_fields | “” | Fields excluded from Kanban grouping settings. |
For example:
kanban_url = reverse_lazy("contacts:contact_kanban_view")
group_by_url = reverse_lazy("contacts:contact_group_by_view")
card_url = reverse_lazy("contacts:contact_card_view")
The navbar only displays a layout option when its corresponding URL is configured. So if a module doesn’t support Kanban, you don’t need another setting to disable it. Simply leave kanban_url empty.
This makes it easy to configure each module according to the views it actually supports.
Filtering and Search
Filtering and searching are handled through a few simple attributes.
| Variable | Default | Purpose |
| filterset_class | None | Connects the navbar to a HorillaFilterSet. |
| filter_option | True | Shows or hides the filter button. |
| search_option | True | Shows or hides the search field. |
| search_push_url | True | Updates the browser URL when searching. |
| enable_quick_filters | False | Enables the Add Quick Filter option. |
For a Contacts page, for example:
filterset_class = ContactFilter
enable_quick_filters = True
The filter fields themselves are not rendered directly inside the navbar. The navbar provides the filter control and works with the filter panel through htmx.
This keeps the navbar and filter implementation separate while allowing them to work together.
Actions and Buttons
You can also configure actions and buttons shown on the right side of the navbar.
| Variable | Default | Purpose |
| enable_actions | False | Enables the actions dropdown. |
| column_selector_exclude_fields | [] | Removes selected fields from the Add Column selector. |
| new_button | None | Defines the primary action button. |
| second_button | None | Defines a secondary button. |
| reload_option | True | Shows or hides the reload button. |
The new_button property is usually created as a @cached_property. This allows the button to be displayed only when the current user has the required permission.
For example:
from django.utils.functional import cached_property
@cached_property
def new_button(self):
if self.request.user.has_perm("contacts.add_contact") or self.request.user.has_perm(
"contacts.add_own_contact"
):
return {
"url": f"""{reverse_lazy("contacts:contact_create_form")}?new=true""",
"attrs": {"id": "contact-create"},
}
return None
If the user doesn’t have either permission, the property returns None, so the New button isn’t rendered.
This is useful because permission handling stays close to the action itself instead of having to duplicate the permission check in the template.
Layout and Indication
There are also a few attributes that control the overall appearance and navigation behavior of the navbar.
| Variable | Default | Purpose |
| nav_width | True | Controls the width and header layout. |
| border_enabled | True | Controls the bottom border. |
| navbar_indication | False | Adds a back-arrow button. |
| navbar_indication_attrs | {} | Defines attributes for the back-arrow button. |
navbar_indication is particularly useful on pages where the user needs to return to a parent page, such as a detail or settings screen.
3. Connecting the Filter Set
A navbar can use the same filter system as the associated list view.
For example, a Contacts filter can be defined using HorillaFilterSet:
from horilla.contrib.generics.filters import HorillaFilterSet
from .models import Contact
class ContactFilter(HorillaFilterSet):
class Meta:
model = Contact
fields = [
"title",
"contact_source",
"address_state",
]
Then connect it to the navbar:
filterset_class = ContactFilter
The navbar doesn’t need to know how every filter field works. It only needs the filter set class.
This also keeps the configuration consistent between HorillaNavView and the corresponding HorillaListView.
4. Adding a Permission-Aware Actions Menu
If a page needs additional actions, you can enable the actions menu:
enable_actions = True
enable_quick_filters = True
column_selector_exclude_fields = ["message_id", "is_convert"]
Once enable_actions is enabled, Horilla can build the available actions based on the current layout and the user’s permissions.
For example, the actions menu can contain options related to importing records, adding columns, or creating quick filters. Depending on the selected layout, it can also provide settings for Kanban, Group By, or Timeline views.
You don’t have to manually build each of these menu items. The navbar uses the configured URLs and options to determine what should be available.
This keeps the action menu consistent between modules.
5. Putting Everything Together
A more complete Contacts navbar can combine the different options we’ve covered:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from horilla.contrib.generics.views import HorillaNavView
from horilla.decorators import htmx_required, permission_required
from .filters import ContactFilter
@method_decorator(htmx_required, name="dispatch")
@method_decorator(
permission_required(
["contacts.view_contact", "contacts.view_own_contact"]
),
name="dispatch",
)
class ContactNavbar(LoginRequiredMixin, HorillaNavView):
"""Navbar View for Contact page"""
search_url = reverse_lazy("contacts:contact_list_view")
main_url = reverse_lazy("contacts:contacts_view")
kanban_url = reverse_lazy("contacts:contact_kanban_view")
group_by_url = reverse_lazy("contacts:contact_group_by_view")
card_url = reverse_lazy("contacts:contact_card_view")
split_view_url = reverse_lazy("contacts:contact_split_view")
chart_url = reverse_lazy("contacts:contact_chart_view")
timeline_url = reverse_lazy("contacts:contacts_timeline")
model_name = "Contact"
model_app_label = "contacts"
filterset_class = ContactFilter
exclude_kanban_fields = "company"
enable_actions = True
enable_quick_filters = True
@cached_property
def new_button(self):
"""Create a new contact button"""
if self.request.user.has_perm(
"contacts.add_contact"
) or self.request.user.has_perm("contacts.add_own_contact"):
return {
"url": f"""{reverse_lazy("contacts:contact_create_form")}?new=true""",
"attrs": {"id": "contact-create"},
}
return None
With this configuration, the Contacts navbar can provide search, filters, quick filters, saved views, multiple layouts, the actions menu, and a permission-controlled New button.
The important thing is that none of the navbar’s HTML has to be written in the view.
6. Why Use HorillaNavView?
Without a generic navbar, each module would need to handle many of the same pieces independently:
- Page title
- Search
- Filters
- Saved views
- Layout switching
- Reload
- Actions
- Permission checks
- htmx targets and requests
- New-record buttons
That approach quickly leads to repeated templates and view logic.
With HorillaNavView, the common behavior stays in one place. Individual modules only provide the configuration that is specific to them.
For example, a Contacts page might enable several layouts:
kanban_url = reverse_lazy(“contacts:contact_kanban_view”)
card_url = reverse_lazy(“contacts:contact_card_view”)
timeline_url = reverse_lazy(“contacts:contacts_timeline”)
while a settings page can disable almost everything and only keep the back button.
This makes the implementation easier to maintain as more modules are added.
HorillaNavView provides a common way to build navigation bars for Horilla pages without creating the same navbar markup repeatedly.
For a typical list page, you can connect the model, filter set, search URL, main URL, supported layouts, actions, and permission-aware New button. For a detail or settings page, you can disable the list-specific features and keep only the controls that are actually needed.
The main attributes you’ll use most often are:
- model_name
- model_app_label
- filterset_class
- search_url
- main_url
- kanban_url
- card_url
- group_by_url
- timeline_url
- enable_actions
- new_button
- navbar_indication
You don’t need to configure every option. Start with the basic navbar and add features only when the page requires them.
Once the common navbar behavior is handled by HorillaNavView, adding navigation to another Horilla module becomes much simpler. The module-specific view only needs to define its URLs, permissions, filters, and the features it wants to expose.