If you have worked with Horilla, you may have noticed the circular “+” button at the bottom-left of the sidebar. Clicking it expands a set of small buttons that let users quickly create records such as Leads, Accounts, Contacts, and more.
This is Horilla’s floating menu.
Like several other menu systems in Horilla CRM, the floating menu is registry-based. Instead of adding every button directly to the sidebar, each app registers its own menu item. This keeps the core sidebar independent of individual apps and makes it easier to add new actions.
Understanding the Floating Menu Registry
The registry is defined in horilla/menu/floating_menu.py. It maintains a simple list of registered classes:
floating_registry: List[Any] = []
def register(cls):
"""Decorator to register a floating menu class."""
floating_registry.append(cls)
return cls
The @floating_menu.register decorator adds a class to this list. Later, get_floating_menu() goes through the registered classes, collects their configuration, and checks the permissions for the current user.
Unlike some other Horilla registries, the floating menu doesn’t have an order option. The buttons appear according to their registration order.
Registering a Floating Menu Item
A good example can be found in horilla_crm/leads/menu.py:
from horilla.menu import floating_menu
from horilla.urls import reverse_lazy
from .models import Lead
@floating_menu.register
class LeadFloating:
title = Lead()._meta.verbose_name
url = reverse_lazy("leads:leads_create")
icon = "/assets/icons/leads.svg"
items = {
"hx-target": "#modalBox",
"hx-swap": "innerHTML",
"onclick": "openModal()",
"perm": ["leads.add_lead", "leads.add_own_lead"],
}
The class is intentionally small. title provides the tooltip text, while url points to the create form. reverse_lazy() is used because the menu module can be imported before the URL configuration is completely resolved.
The icon points to the SVG displayed in the floating button.
The items dictionary is particularly important. It contains the HTML attributes needed by the sidebar, including HTMX attributes and the permission information.
The Permission Gotcha
There is one detail that can easily cause confusion when creating a floating menu item.
The permission is stored inside items:
“perm”: [“leads.add_lead”, “leads.add_own_lead”]
If multiple permissions are provided, Horilla uses OR logic. The user only needs one of the listed permissions for the button to be displayed.
However, unlike my_settings_menu, a floating menu item without a perm value won’t appear. The registry requires a non-empty permission list before adding the item.
There is also no separate condition hook for floating menu items. Permission is the main visibility check.
Don’t Forget auto_import_modules
Registering a class only works if the module containing it is actually imported.
Your app configuration should include:
class YourAppConfig(AppLauncher):
auto_import_modules = [“registration”, “signals”, “menu”]
The important part is “menu”.
If it is missing, menu.py won’t be imported, so the decorated class never gets registered. The result can be confusing because the application may run normally—the floating button simply won’t show up.
The build_app scaffolding normally adds this configuration automatically.
From Registry to Sidebar
Once registered, the menu is exposed globally through menu_context_processor:
“floating_menu”: get_floating_menu(request),
The sidebar then loops through the available items:
{% for menu in floating_menu %}
<li
hx-get="{{ menu.url }}?{{ request.GET.urlencode }}"
{% for key, value in menu.items.items %}
{{ key }}="{{ value }}"
{% endfor %}
title="{{ menu.title }}"
>
<img src="{% static menu.icon %}" alt="">
</li>
{% endfor %}
Notice that the template writes every key from menu.items directly onto the <li> element. That’s why attributes such as hx-target, hx-swap, and onclick need to be placed inside items.
Opening the Create Form
When the user clicks the button, HTMX loads the configured URL into #modalBox.
At the same time, onclick=”openModal()” calls the helper from global.js:
function openModal() {
ModalManager.open("dbmodal", "modalBox");
}
So the process is straightforward: the user clicks the floating button, HTMX loads the form, and openModal() displays it inside the modal.
One CSS Limitation
The floating menu animation is handled by CSS rather than JavaScript. The stylesheet defines positions using selectors such as:
.floating-menu input:checked~.floating-nav li:nth-child(1) {
transform: translatey(-48px);
}
Currently, the CSS provides offsets for only the first five items. If you register a sixth floating menu item, it may appear but won’t receive an expansion position. In that case, you’ll need to add another nth-child() rule.
Horilla’s floating menu is a good example of its registry-based architecture. Each app can register its own quick-create action without modifying the main sidebar.
To add one yourself, create a registered class, provide the title, URL, icon, and required permission, and make sure your app imports the menu module. If you’re adding more than five items, remember to update the CSS as well.
Once these pieces are in place, Horilla handles the rest—from discovering the menu item to checking permissions, loading the form with HTMX, and displaying it in the modal.