Skip to content

Breadcrumbs Component

Version: 0.6.0
Last Updated: January 3, 2026

Breadcrumbs provide hierarchical navigation showing the user's current location within the application and allow navigation back to parent pages.


Overview

The breadcrumbs system in Arctyk ITSM uses a flexible, multi-approach architecture:

  1. Context Processor - Automatic breadcrumb generation from URL paths
  2. Manual Context - Explicit breadcrumb definition in views
  3. Mixin Pattern - Reusable breadcrumb logic via BreadcrumbMixin
  4. Template Tag - Programmatic breadcrumb building

Architecture

Template

Location: src/templates/partials/breadcrumbs.html

{% if breadcrumbs %}
<nav aria-label="breadcrumb" class="mb-3">
  <ol class="breadcrumb small">
    {% for crumb in breadcrumbs %}
    <li class="breadcrumb-item {% if not crumb.url %}active{% endif %}">
      {% if crumb.url %}
      <a href="{{ crumb.url }}" class="text-decoration-none"
        >{{ crumb.label }}</a
      >
      {% else %}
      <span class="text-muted">{{ crumb.label }}</span>
      {% endif %}
    </li>
    {% endfor %}
  </ol>
</nav>
{% endif %}

Key Features:

  • Semantic HTML with <nav> and <ol>
  • ARIA label for accessibility
  • Last item marked as active (current page)
  • Clickable ancestors with links
  • Uses Bootstrap 5 breadcrumb styling

Context Processor

Location: src/config/context_processors.py

Automatically generates breadcrumbs from URL paths. Active on all templates.

def breadcrumbs(request):
    """
    Auto-generate breadcrumbs from URL path.
    Can be overridden by explicit 'breadcrumbs' in view context.
    """
    path = request.path.strip("/").split("/")
    crumbs = []
    url = ""

    for segment in path:
        url += "/" + segment
        crumbs.append({
            "name": capfirst(segment.replace("-", " ")),
            "url": url if segment != path[-1] else None,
        })

    # Special handling for specific URL names
    resolved = resolve(request.path)

    if resolved.url_name == "project_detail":
        project_id = resolved.kwargs.get("project_id")
        if project_id:
            project = Project.objects.get(pk=project_id)
            crumbs[-1]["name"] = project.name

    return {"breadcrumbs": crumbs}

Mixin Pattern

Location: src/core/mixins/breadcrumbs.py

Provides structured breadcrumb management with return URL support:

class BreadcrumbMixin:
    """Adds breadcrumbs + return URL support to class-based views."""

    def get_return_url(self):
        return self.request.GET.get("return")

    def get_breadcrumbs(self):
        return []

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        return_url = self.get_return_url()
        breadcrumbs = self.get_breadcrumbs()

        # Attach return URL to breadcrumb links
        if return_url:
            for crumb in breadcrumbs:
                if crumb.get("url"):
                    crumb["url"] += f"?return={return_url}"

        ctx.update({
            "breadcrumbs": breadcrumbs,
            "return_url": return_url,
        })
        return ctx

Template Tag

Location: src/users/templatetags/breadcrumbs.py

Simple tag for programmatic breadcrumb building:

@register.simple_tag(takes_context=True)
def breadcrumb(context, label, url=None):
    """
    Programmatically add a breadcrumb to the context.

    Usage:
        {% breadcrumb "Users" users_url %}
        {% breadcrumb user.get_full_name %}
    """
    crumbs = context.setdefault("breadcrumbs", [])
    crumbs.append({"label": label, "url": url})
    return ""

Data Structure

Breadcrumbs are dictionaries with the following structure:

{
    "label": "Ticket #123",  # Display text
    "url": "/tickets/123/"   # Optional: if None, treated as active/current page
}

Example Breadcrumb Chain

ctx["breadcrumbs"] = [
    {"label": "Tickets", "url": "/tickets/"},           # Clickable
    {"label": "Dashboard", "url": "/tickets/123/"},     # Clickable
    {"label": "Comments", "url": None}                  # Active (current)
]

Result in UI:

Tickets / Dashboard / Comments
   ↑          ↑           ↑
 link       link      text only

Usage Patterns

Define breadcrumbs explicitly in your view:

from django.urls import reverse
from django.views.generic import DetailView
from tickets.models import Ticket

class TicketDetailView(DetailView):
    model = Ticket
    template_name = "tickets/ticket_detail.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ticket = self.object

        ctx["breadcrumbs"] = [
            {"label": "Tickets", "url": reverse("tickets:list")},
            {
                "label": f"Ticket #{ticket.ticket_number or ticket.pk}",
                "url": None  # Active - no URL
            }
        ]
        return ctx

Advantages:

  • ✅ Explicit and clear
  • ✅ Full control over breadcrumb chain
  • ✅ Easy to test
  • ✅ Works with Django templates

Best for:

  • Detail pages
  • Custom navigation hierarchies
  • Pages with specific breadcrumb requirements

Pattern 2: BreadcrumbMixin (For Reusable Views)

Use the mixin for structured breadcrumb handling:

from core.mixins.breadcrumbs import BreadcrumbMixin
from django.urls import reverse
from django.views.generic import ListView
from inventory.models import Asset

class AssetListView(BreadcrumbMixin, ListView):
    model = Asset
    template_name = "inventory/asset_list.html"
    paginate_by = 10

    def get_breadcrumbs(self):
        return [
            {"label": "Assets", "url": reverse("asset_list")}
        ]

Advantages:

  • ✅ DRY - reusable breadcrumb logic
  • ✅ Automatic return URL handling
  • ✅ Clean separation of concerns

Return URL Support:

The mixin automatically appends ?return=url to breadcrumb links:

# If user arrives via ?return=/projects/5/
# Breadcrumb links automatically include return parameter
# Allows navigation back to originating page

class TicketDetailView(BreadcrumbMixin, DetailView):
    def get_breadcrumbs(self):
        return [
            {"label": "Tickets", "url": "/tickets/"},
            {"label": "TKT-123"}
        ]

    # When ?return=/projects/5/ is in URL:
    # Generated breadcrumbs become:
    # - "Tickets" → /tickets/?return=/projects/5/
    # - "TKT-123" → (no URL, active)

Pattern 3: Automatic via Context Processor

Let the framework auto-generate breadcrumbs from URL:

{# No view code needed! #}
{# Breadcrumbs automatically generated from path #}

How it works:

  • Path /tickets/list/Tickets / List
  • Path /users/123/profile/Users / 123 / Profile
  • Last segment has no link (active)

Advantages:

  • ✅ Zero view code
  • ✅ Works everywhere
  • ✅ Good for simple hierarchies

Limitations:

  • ❌ Limited customization
  • ❌ URL-based names often not user-friendly
  • ❌ Doesn't handle object-specific labels

Override any time:

# Manual context overrides auto-generated breadcrumbs
ctx["breadcrumbs"] = [
    {"label": "Custom", "url": "/"}
]

Pattern 4: Template Tag (For Complex Views)

Build breadcrumbs dynamically in templates:

{% load breadcrumbs %}

{% breadcrumb "Users" users_url %}
{% breadcrumb user.get_full_name %}
{% breadcrumb "Tickets" %}

Advantages:

  • ✅ Build breadcrumbs in template
  • ✅ Access to template context

Limitations:

  • ❌ Less testable
  • ❌ Logic in templates (anti-pattern)

Only use when:

  • Breadcrumbs are template-specific
  • You need to access template variables
  • Context cannot provide the data

Common Examples

Ticket List → Detail → Edit

# List view
class TicketListView(ListView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["breadcrumbs"] = [
            {"label": "Tickets", "url": None}  # Active
        ]
        return ctx

# Detail view
class TicketDetailView(DetailView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ticket = self.object
        ctx["breadcrumbs"] = [
            {"label": "Tickets", "url": reverse("tickets:list")},
            {"label": f"TKT-{ticket.ticket_number}", "url": None}
        ]
        return ctx

# Edit view
class TicketUpdateView(UpdateView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ticket = self.object
        ctx["breadcrumbs"] = [
            {"label": "Tickets", "url": reverse("tickets:list")},
            {"label": f"TKT-{ticket.ticket_number}",
             "url": ticket.get_absolute_url()},
            {"label": "Edit", "url": None}
        ]
        return ctx

Project → Tickets → Detail

class ProjectDetailView(DetailView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        project = self.object
        ctx["breadcrumbs"] = [
            {"label": "Projects", "url": reverse("projects:list")},
            {"label": project.name, "url": None}
        ]
        return ctx

class ProjectTicketDetailView(DetailView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ticket = self.object
        project = ticket.project

        ctx["breadcrumbs"] = [
            {"label": "Projects", "url": reverse("projects:list")},
            {"label": project.name,
             "url": project.get_absolute_url()},
            {"label": f"TKT-{ticket.ticket_number}", "url": None}
        ]
        return ctx

Users with Return Navigation

Using BreadcrumbMixin with return support:

class UserDetailView(BreadcrumbMixin, DetailView):
    model = User

    def get_breadcrumbs(self):
        user = self.object
        return [
            {"label": "Users", "url": reverse("users:list")},
            {"label": user.get_full_name() or user.username, "url": None}
        ]

# Usage:
# /users/5/ → Normal breadcrumbs
# /users/5/?return=/projects/3/ → Breadcrumbs include return URL
#    Users link becomes: /users/?return=/projects/3/

Styling

Bootstrap 5 Classes

Breadcrumbs use Bootstrap 5's breadcrumb component:

<nav aria-label="breadcrumb">
  <ol class="breadcrumb">
    <li class="breadcrumb-item"><a href="/">Home</a></li>
    <li class="breadcrumb-item"><a href="/docs">Docs</a></li>
    <li class="breadcrumb-item active" aria-current="page">Breadcrumbs</li>
  </ol>
</nav>

Custom Styling

Location: STATIC_SRC/scss/components/_breadcrumbs.scss

.breadcrumb {
  --bs-breadcrumb-padding-x: 0;
  --bs-breadcrumb-padding-y: 0;
  --bs-breadcrumb-margin-bottom: 1rem;
  --bs-breadcrumb-divider: "/";
}

.breadcrumb-item {
  &.active {
    color: var(--bs-breadcrumb-item-active-color);
  }

  a {
    text-decoration: none;

    &:hover {
      text-decoration: underline;
    }
  }
}

Size Variant

Default breadcrumb uses small class for compact display:

<ol class="breadcrumb small">
  {# ... #}
</ol>

Accessibility

ARIA Attributes

<nav aria-label="breadcrumb">
  {# Semantic nav #}
  <li aria-current="page">Current</li>
  {# Marks active page #}
</nav>

Semantic HTML

  • Uses <nav> for navigation section
  • Uses <ol> for ordered list (hierarchy matters)
  • Last item is not a link (no false clickability)

Keyboard Navigation

  • Tab through breadcrumb links
  • Enter/Space to follow links
  • Screen readers announce as navigation

Common Patterns

Pattern: Three-Level Hierarchy

App → Module → Feature → Item
Tickets → In Progress → TKT-123 → Comments
ctx["breadcrumbs"] = [
    {"label": "Tickets", "url": "/tickets/"},
    {"label": "In Progress", "url": "/tickets/?status=in_progress"},
    {"label": f"TKT-{ticket.pk}", "url": ticket.get_absolute_url()},
    {"label": "Comments", "url": None}
]

Pattern: Deep Nesting

For very deep hierarchies, consider truncation:

def get_breadcrumbs(self):
    crumbs = [
        {"label": "Home", "url": "/"},
        {"label": "...", "url": None},  # Truncate middle
        {"label": "Parent", "url": parent_url},
        {"label": "Current", "url": None}
    ]
    return crumbs

Pattern: Dynamic Labels

Use object names instead of URL slugs:

# ❌ Don't:
{"label": "ticket-management-system"}

# ✅ Do:
{"label": f"TKT-{ticket.ticket_number}"}
{"label": project.name}
{"label": user.get_full_name() or user.username}

Best Practices

✅ Do

  1. Keep it simple - 3-4 levels max
  2. Use meaningful labels - Show object names, not URLs
  3. Always make ancestors clickable - Enable back navigation
  4. Never make current page clickable - No dead links
  5. Use consistent patterns - Same hierarchy throughout app
  6. Test accessibility - Keyboard navigation and screen readers
  7. Override when needed - Don't be forced by context processor

❌ Don't

  1. Don't truncate important items - Breadcrumbs are for navigation
  2. Don't use all-caps - Use normal capitalization
  3. Don't add unnecessary levels - Keep it shallow
  4. Don't make breadcrumbs too long - Think responsive design
  5. Don't hide breadcrumbs on mobile - They're navigation!
  6. Don't include Home in breadcrumbs - Logo usually does that
  7. Don't have links to page you're on - Avoid circular navigation

Testing

Unit Tests

import pytest
from django.urls import reverse
from tickets.models import Ticket

@pytest.mark.django_db
class TestTicketBreadcrumbs:
    def test_ticket_list_breadcrumbs(self, client, user):
        client.force_login(user)
        response = client.get(reverse('tickets:list'))

        assert 'breadcrumbs' in response.context
        breadcrumbs = response.context['breadcrumbs']
        assert breadcrumbs[0]['label'] == 'Tickets'
        assert breadcrumbs[0]['url'] is None  # Active

    def test_ticket_detail_breadcrumbs(self, client, user):
        ticket = Ticket.objects.create(title='Test', requester=user)
        client.force_login(user)
        response = client.get(ticket.get_absolute_url())

        breadcrumbs = response.context['breadcrumbs']
        assert len(breadcrumbs) == 2
        assert breadcrumbs[0]['label'] == 'Tickets'
        assert 'TKT-' in breadcrumbs[1]['label']
        assert breadcrumbs[1]['url'] is None  # Active

    def test_breadcrumbs_with_return_url(self, client, user):
        ticket = Ticket.objects.create(title='Test', requester=user)
        client.force_login(user)

        # Include return URL
        url = f"{ticket.get_absolute_url()}?return=/projects/5/"
        response = client.get(url)

        breadcrumbs = response.context['breadcrumbs']
        # First link should include return parameter
        assert '?return=' in breadcrumbs[0]['url']

Integration Tests

def test_breadcrumb_navigation(client, user):
    """Test that clicking breadcrumb links works."""
    ticket = Ticket.objects.create(title='Test', requester=user)
    client.force_login(user)

    # Navigate to detail page
    response = client.get(ticket.get_absolute_url())
    assert response.status_code == 200

    # Click first breadcrumb (Tickets list)
    tickets_url = response.context['breadcrumbs'][0]['url']
    response = client.get(tickets_url)
    assert response.status_code == 200
    assert 'Test' in response.content.decode()  # Ticket visible in list

FAQ

Q: Should I use BreadcrumbMixin or manual context?

A: Use what's clearest:

  • BreadcrumbMixin: When you have multiple views with similar breadcrumbs
  • Manual context: For one-off views with specific needs
  • Both approaches work equally well

Q: Can I have dynamic breadcrumbs based on query parameters?

A: Yes! Build them in the view:

def get_context_data(self, **kwargs):
    ctx = super().get_context_data(**kwargs)
    status = self.request.GET.get('status', 'all')

    crumbs = [{"label": "Tickets", "url": "/tickets/"}]
    if status != 'all':
        crumbs.append({
            "label": f"Status: {status.title()}",
            "url": None
        })

    ctx["breadcrumbs"] = crumbs
    return ctx

Q: What if the auto-generated breadcrumbs are wrong?

A: Override them:

def get_context_data(self, **kwargs):
    ctx = super().get_context_data(**kwargs)
    ctx["breadcrumbs"] = [  # This overrides auto-generated
        {"label": "Dashboard", "url": "/"},
        {"label": self.object.title, "url": None}
    ]
    return ctx

Q: Should breadcrumbs include the home/dashboard page?

A: Generally no - the logo/home link usually serves that purpose. Keep breadcrumbs for showing location within a section.

Q: How do I handle breadcrumbs for modal/popup views?

A: Don't include breadcrumbs in modals - they don't need navigation since they're contextual to the current page.


Resources


Happy navigating! 🧭