Skip to main content

Sidebar Navigation

The sidebar navigation in Django Unfold provides a customizable menu system for organizing and accessing your admin interface. Configure custom navigation groups, links, icons, badges, and permissions to create an intuitive admin experience.

Basic Configuration

Define your sidebar navigation in the UNFOLD settings dictionary using the SIDEBAR key.
settings.py
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _

UNFOLD = {
    "SIDEBAR": {
        "show_search": False,              # Search in application/model names
        "command_search": False,           # Use command palette for search
        "show_all_applications": False,    # Show dropdown with all apps
        "navigation": [
            {
                "title": _("Navigation"),
                "separator": True,
                "items": [
                    {
                        "title": _("Dashboard"),
                        "icon": "dashboard",
                        "link": reverse_lazy("admin:index"),
                    },
                ],
            },
        ],
    },
}
Navigation is organized into groups, with each group containing multiple items.
title
string
required
Display name for the navigation group
separator
bool
default:"False"
Show a top border separator for the group
collapsible
bool
default:"False"
Make the group expandable/collapsible
items
list
required
List of navigation items in the group
badge
string | callable
Badge text or callback function for the group
title
string | callable
required
Display name for the navigation item
icon
string
required
Material Icon name from Google Fonts Icons
URL for the navigation item
badge
string | callable
Badge text or callback function
badge_variant
string
default:"info"
Badge color variant: info, success, warning, primary, danger
badge_style
string
default:"solid"
Badge background style: solid or outline
permission
callable
Permission check callback function
active
callable
Custom active state callback function

Basic Examples

Simple Navigation

settings.py
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _

UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("Main Navigation"),
                "items": [
                    {
                        "title": _("Dashboard"),
                        "icon": "dashboard",
                        "link": reverse_lazy("admin:index"),
                    },
                    {
                        "title": _("Customers"),
                        "icon": "people",
                        "link": reverse_lazy("admin:customers_customer_changelist"),
                    },
                    {
                        "title": _("Orders"),
                        "icon": "shopping_cart",
                        "link": reverse_lazy("admin:orders_order_changelist"),
                    },
                    {
                        "title": _("Products"),
                        "icon": "inventory",
                        "link": reverse_lazy("admin:products_product_changelist"),
                    },
                ],
            },
        ],
    },
}

Collapsible Groups

settings.py
UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("Content Management"),
                "separator": True,
                "collapsible": True,  # Make group collapsible
                "items": [
                    {
                        "title": _("Pages"),
                        "icon": "article",
                        "link": reverse_lazy("admin:cms_page_changelist"),
                    },
                    {
                        "title": _("Blog Posts"),
                        "icon": "edit_note",
                        "link": reverse_lazy("admin:blog_post_changelist"),
                    },
                    {
                        "title": _("Media Library"),
                        "icon": "photo_library",
                        "link": reverse_lazy("admin:media_file_changelist"),
                    },
                ],
            },
            {
                "title": _("Settings"),
                "separator": True,
                "collapsible": True,
                "items": [
                    {
                        "title": _("Site Configuration"),
                        "icon": "settings",
                        "link": reverse_lazy("admin:config_sitesettings_changelist"),
                    },
                    {
                        "title": _("Users"),
                        "icon": "manage_accounts",
                        "link": reverse_lazy("admin:auth_user_changelist"),
                    },
                ],
            },
        ],
    },
}

Advanced Features

Dynamic Badges

Display dynamic badges with counts or notifications:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("Operations"),
                "items": [
                    {
                        "title": _("Pending Orders"),
                        "icon": "pending",
                        "link": reverse_lazy("admin:orders_order_changelist") + "?status=pending",
                        "badge": "app.utils.pending_orders_count",
                        "badge_variant": "warning",
                    },
                    {
                        "title": _("Support Tickets"),
                        "icon": "support",
                        "link": reverse_lazy("admin:support_ticket_changelist"),
                        "badge": "app.utils.open_tickets_count",
                        "badge_variant": "danger",
                    },
                ],
            },
        ],
    },
}
utils.py
from orders.models import Order
from support.models import Ticket

def pending_orders_count(request):
    """Return count of pending orders."""
    return Order.objects.filter(status='pending').count()

def open_tickets_count(request):
    """Return count of open support tickets."""
    return Ticket.objects.filter(status='open').count()

Permission-Based Navigation

Show/hide navigation items based on user permissions:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("Admin Tools"),
                "items": [
                    {
                        "title": _("User Management"),
                        "icon": "manage_accounts",
                        "link": reverse_lazy("admin:auth_user_changelist"),
                        "permission": "app.utils.is_admin",
                    },
                    {
                        "title": _("System Logs"),
                        "icon": "history",
                        "link": reverse_lazy("admin:logs_systemlog_changelist"),
                        "permission": lambda request: request.user.is_superuser,
                    },
                    {
                        "title": _("Reports"),
                        "icon": "analytics",
                        "link": reverse_lazy("admin:reports_index"),
                        "permission": "app.utils.can_view_reports",
                    },
                ],
            },
        ],
    },
}
utils.py
def is_admin(request):
    """Check if user is an admin."""
    return request.user.groups.filter(name='Admins').exists()

def can_view_reports(request):
    """Check if user can view reports."""
    return request.user.has_perm('reports.view_report')
Generate navigation links dynamically:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("Quick Actions"),
                "items": [
                    {
                        "title": _("My Tasks"),
                        "icon": "task",
                        "link": "app.utils.get_user_tasks_link",
                    },
                    {
                        "title": _("My Reports"),
                        "icon": "description",
                        "link": "app.utils.get_user_reports_link",
                    },
                ],
            },
        ],
    },
}
utils.py
from django.urls import reverse

def get_user_tasks_link(request):
    """Generate link to user's tasks."""
    return reverse('admin:tasks_task_changelist') + f"?assigned_to={request.user.id}"

def get_user_reports_link(request):
    """Generate link to user's reports."""
    return reverse('admin:reports_report_changelist') + f"?created_by={request.user.id}"

Nested Navigation

Create nested navigation items:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "navigation": [
            {
                "title": _("E-commerce"),
                "collapsible": True,
                "items": [
                    {
                        "title": _("Products"),
                        "icon": "inventory",
                        "link": reverse_lazy("admin:products_product_changelist"),
                    },
                    {
                        "title": _("Orders"),
                        "icon": "shopping_cart",
                        "items": [  # Nested items
                            {
                                "title": _("All Orders"),
                                "link": reverse_lazy("admin:orders_order_changelist"),
                            },
                            {
                                "title": _("Pending"),
                                "link": reverse_lazy("admin:orders_order_changelist") + "?status=pending",
                            },
                            {
                                "title": _("Completed"),
                                "link": reverse_lazy("admin:orders_order_changelist") + "?status=completed",
                            },
                        ],
                    },
                ],
            },
        ],
    },
}

Search Configuration

Enable simple search in application and model names:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "show_search": True,
    },
}
Replace sidebar search with the more powerful command palette:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "show_search": True,
        "command_search": True,  # Use command palette instead
    },
}
The command palette provides advanced search functionality including model data search and custom callbacks. See the Command Palette documentation for details.

Show All Applications

Display a dropdown with all registered applications:
settings.py
UNFOLD = {
    "SIDEBAR": {
        "show_all_applications": True,
    },
}
This option is useful when you have many applications but only show the most important ones in the custom navigation.

Complete Configuration Example

settings.py
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _

UNFOLD = {
    "SIDEBAR": {
        "show_search": True,
        "command_search": True,
        "show_all_applications": False,
        "navigation": [
            {
                "title": _("Dashboard"),
                "separator": True,
                "items": [
                    {
                        "title": _("Home"),
                        "icon": "dashboard",
                        "link": reverse_lazy("admin:index"),
                    },
                    {
                        "title": _("Analytics"),
                        "icon": "analytics",
                        "link": reverse_lazy("admin:analytics_dashboard"),
                        "permission": lambda request: request.user.has_perm('analytics.view_dashboard'),
                    },
                ],
            },
            {
                "title": _("Sales"),
                "separator": True,
                "collapsible": True,
                "badge": "app.utils.pending_orders_badge",
                "items": [
                    {
                        "title": _("Orders"),
                        "icon": "shopping_cart",
                        "link": reverse_lazy("admin:orders_order_changelist"),
                        "badge": "app.utils.pending_orders_count",
                        "badge_variant": "warning",
                    },
                    {
                        "title": _("Customers"),
                        "icon": "people",
                        "link": reverse_lazy("admin:customers_customer_changelist"),
                    },
                    {
                        "title": _("Invoices"),
                        "icon": "receipt",
                        "link": reverse_lazy("admin:invoices_invoice_changelist"),
                    },
                ],
            },
            {
                "title": _("Inventory"),
                "separator": True,
                "collapsible": True,
                "items": [
                    {
                        "title": _("Products"),
                        "icon": "inventory",
                        "link": reverse_lazy("admin:products_product_changelist"),
                    },
                    {
                        "title": _("Categories"),
                        "icon": "category",
                        "link": reverse_lazy("admin:products_category_changelist"),
                    },
                    {
                        "title": _("Stock Alerts"),
                        "icon": "warning",
                        "link": reverse_lazy("admin:inventory_stockalert_changelist"),
                        "badge": "app.utils.low_stock_count",
                        "badge_variant": "danger",
                    },
                ],
            },
            {
                "title": _("Administration"),
                "separator": True,
                "collapsible": True,
                "items": [
                    {
                        "title": _("Users"),
                        "icon": "manage_accounts",
                        "link": reverse_lazy("admin:auth_user_changelist"),
                        "permission": lambda request: request.user.is_superuser,
                    },
                    {
                        "title": _("Groups"),
                        "icon": "group",
                        "link": reverse_lazy("admin:auth_group_changelist"),
                        "permission": lambda request: request.user.is_superuser,
                    },
                ],
            },
        ],
    },
}

# Callback functions in utils.py
# def pending_orders_count(request):
#     from orders.models import Order
#     return Order.objects.filter(status='pending').count()
#
# def pending_orders_badge(request):
#     count = pending_orders_count(request)
#     return count if count > 0 else None
#
# def low_stock_count(request):
#     from inventory.models import StockAlert
#     return StockAlert.objects.filter(resolved=False).count()

Best Practices

Always use reverse_lazy() instead of reverse() in settings to avoid circular import issues.
from django.urls import reverse_lazy  # Good
from django.urls import reverse       # Bad in settings
Group navigation items based on user workflows and common tasks rather than by model hierarchy.
# Good - organized by workflow
["Dashboard", "Orders", "Customers", "Products"]

# Less optimal - organized by app
["Auth", "Orders App", "Products App", "Customers App"]
Choose icons that clearly represent the navigation item’s purpose. Browse Material Icons for options.
Cache expensive badge calculations to avoid performance issues.
from django.core.cache import cache

def pending_orders_count(request):
    cache_key = f'pending_orders_{request.user.id}'
    count = cache.get(cache_key)
    
    if count is None:
        count = Order.objects.filter(status='pending').count()
        cache.set(cache_key, count, 60)  # Cache for 1 minute
    
    return count
Always test permission callbacks to ensure users only see appropriate navigation items.

Command Palette

Configure advanced search and navigation

Custom Pages

Add custom pages to your sidebar navigation

Tabs

Configure tab navigation for changeforms

Settings

View all available configuration options

Build docs developers (and LLMs) love