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:
- Context Processor - Automatic breadcrumb generation from URL paths
- Manual Context - Explicit breadcrumb definition in views
- Mixin Pattern - Reusable breadcrumb logic via
BreadcrumbMixin - 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¶
Breadcrumb Object¶
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:
Usage Patterns¶
Pattern 1: Manual Context (Recommended)¶
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:
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:
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¶
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¶
- Keep it simple - 3-4 levels max
- Use meaningful labels - Show object names, not URLs
- Always make ancestors clickable - Enable back navigation
- Never make current page clickable - No dead links
- Use consistent patterns - Same hierarchy throughout app
- Test accessibility - Keyboard navigation and screen readers
- Override when needed - Don't be forced by context processor
❌ Don't¶
- Don't truncate important items - Breadcrumbs are for navigation
- Don't use all-caps - Use normal capitalization
- Don't add unnecessary levels - Keep it shallow
- Don't make breadcrumbs too long - Think responsive design
- Don't hide breadcrumbs on mobile - They're navigation!
- Don't include Home in breadcrumbs - Logo usually does that
- 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! 🧭