Topbar Component¶
Version: 0.6.0
Last Updated: January 3, 2026
Location: src/templates/partials/topbar.html
The topbar provides the top navigation bar with key user actions, search functionality, notifications, and user profile access. It spans the full width below the main sidebar.
Overview¶
The topbar includes:
- Logo/branding area
- Search box with real-time results
- Create button (primary action)
- Notifications dropdown
- User profile menu
- Responsive collapse on mobile
- Quick links
Basic Structure¶
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom">
<div class="container-fluid">
<!-- Logo -->
<span class="navbar-brand">
<i class="bi bi-grid-3x3-gap"></i>
</span>
<!-- Search -->
<div class="flex-grow-1 mx-3">
<input
type="search"
class="form-control form-control-sm"
placeholder="Search tickets..."
/>
</div>
<!-- Right Section -->
<div class="navbar-nav ms-auto">
<!-- Create Button -->
<a href="{% url 'tickets:ticket_create' %}" class="nav-link">
<button class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle"></i> Create
</button>
</a>
<!-- Notifications -->
<div class="nav-item dropdown">
<a
href="#"
class="nav-link position-relative"
data-bs-toggle="dropdown"
>
<i class="bi bi-bell"></i>
<span class="badge bg-danger position-absolute">3</span>
</a>
<div class="dropdown-menu dropdown-menu-end">
<h6 class="dropdown-header">Notifications</h6>
<a href="#" class="dropdown-item">New ticket assigned</a>
</div>
</div>
<!-- User Menu -->
<div class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<img
src="{{ user.profile.avatar.url }}"
alt="{{ user.first_name }}"
class="rounded-circle"
width="32"
/>
</a>
<div class="dropdown-menu dropdown-menu-end">
<h6 class="dropdown-header">{{ user.get_full_name }}</h6>
<a href="{% url 'users:user_profile' %}" class="dropdown-item">
Profile
</a>
<hr class="dropdown-divider" />
<a href="{% url 'admin:logout' %}" class="dropdown-item"> Logout </a>
</div>
</div>
</div>
</div>
</nav>
Search Functionality¶
Search Input¶
<div class="search-wrapper flex-grow-1 mx-3">
<input
type="search"
class="form-control form-control-sm"
id="globalSearch"
placeholder="Search tickets, projects, users..."
autocomplete="off"
/>
<div id="searchResults" class="search-results"></div>
</div>
JavaScript¶
document.getElementById("globalSearch").addEventListener("input", function (e) {
const query = e.target.value;
const resultsDiv = document.getElementById("searchResults");
if (query.length < 2) {
resultsDiv.innerHTML = "";
return;
}
fetch(`/api/search/?q=${encodeURIComponent(query)}`)
.then((response) => response.json())
.then((data) => {
resultsDiv.innerHTML = data.results
.map(
(item) => `
<a href="${item.url}" class="search-result">
<i class="bi bi-${item.icon}"></i>
${item.title}
</a>
`
)
.join("");
});
});
View Implementation¶
from django.http import JsonResponse
from tickets.models import Ticket
from projects.models import Project
from django.contrib.auth.models import User
def search(request):
q = request.GET.get('q', '')
results = []
if q:
# Search tickets
tickets = Ticket.objects.filter(title__icontains=q)[:3]
results.extend([
{
'type': 'ticket',
'icon': 'ticket',
'title': f'#{t.ticket_number}: {t.title}',
'url': f'/tickets/{t.pk}/'
}
for t in tickets
])
# Search projects
projects = Project.objects.filter(name__icontains=q)[:3]
results.extend([
{
'type': 'project',
'icon': 'folder',
'title': p.name,
'url': f'/projects/{p.pk}/'
}
for p in projects
])
return JsonResponse({'results': results})
Create Button¶
Dropdown Menu¶
<div class="nav-item dropdown">
<button
class="btn btn-primary btn-sm dropdown-toggle"
data-bs-toggle="dropdown"
>
<i class="bi bi-plus-circle"></i> Create
</button>
<div class="dropdown-menu">
<a href="{% url 'tickets:ticket_create' %}" class="dropdown-item">
<i class="bi bi-ticket"></i> Ticket
</a>
<a href="{% url 'projects:project_create' %}" class="dropdown-item">
<i class="bi bi-folder"></i> Project
</a>
<hr class="dropdown-divider" />
<a href="{% url 'users:user_create' %}" class="dropdown-item">
<i class="bi bi-person"></i> User
</a>
</div>
</div>
Notifications¶
Badge with Count¶
<div class="nav-item dropdown">
<a
href="#"
class="nav-link position-relative"
data-bs-toggle="dropdown"
title="Notifications"
>
<i class="bi bi-bell"></i>
{% if unread_notifications_count > 0 %}
<span
class="badge bg-danger position-absolute top-0 start-100 translate-middle"
>
{{ unread_notifications_count }}
</span>
{% endif %}
</a>
<div class="dropdown-menu dropdown-menu-end" style="width: 350px;">
<h6 class="dropdown-header">Notifications</h6>
{% for notification in notifications %}
<a href="{{ notification.url }}" class="dropdown-item">
<small>{{ notification.message }}</small>
<div class="text-muted">
<small>{{ notification.created_at|timesince }} ago</small>
</div>
</a>
{% endfor %}
<hr class="dropdown-divider" />
<a
href="{% url 'notifications:list' %}"
class="dropdown-item text-center text-primary"
>
View all
</a>
</div>
</div>
User Profile Menu¶
Dropdown Menu¶
<div class="nav-item dropdown">
<a
href="#"
class="nav-link dropdown-toggle d-flex align-items-center"
data-bs-toggle="dropdown"
role="button"
aria-haspopup="true"
>
<img
src="{{ user.profile.avatar.url }}"
alt="{{ user.get_full_name }}"
class="rounded-circle me-2"
width="32"
height="32"
/>
<span class="d-none d-md-inline">{{ user.first_name }}</span>
</a>
<div class="dropdown-menu dropdown-menu-end">
<h6 class="dropdown-header">{{ user.get_full_name }}</h6>
<small class="dropdown-header text-muted">{{ user.email }}</small>
<hr class="dropdown-divider" />
<a href="{% url 'users:user_profile' %}" class="dropdown-item">
<i class="bi bi-person"></i> Profile
</a>
<a href="{% url 'users:user_settings' %}" class="dropdown-item">
<i class="bi bi-gear"></i> Settings
</a>
<hr class="dropdown-divider" />
<a href="{% url 'help' %}" class="dropdown-item">
<i class="bi bi-question-circle"></i> Help
</a>
<a href="{% url 'admin:logout' %}" class="dropdown-item text-danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</div>
Responsive Behavior¶
Mobile Menu¶
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom">
<div class="container-fluid">
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#topbarNav"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="topbarNav">
<!-- Topbar content -->
</div>
</div>
</nav>
CSS Media Queries¶
@media (max-width: 768px) {
.navbar-brand {
margin-right: auto;
}
.navbar-collapse {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border-bottom: 1px solid #dee2e6;
}
.search-wrapper {
width: 100%;
margin: 1rem 0 !important;
}
}
Testing¶
Component Tests¶
from django.test import TestCase, Client
from django.contrib.auth.models import User
class TopbarTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass',
first_name='Test'
)
self.client = Client()
def test_topbar_renders(self):
self.client.login(username='testuser', password='testpass')
response = self.client.get('/tickets/')
self.assertContains(response, 'topbar')
def test_user_name_visible(self):
self.client.login(username='testuser', password='testpass')
response = self.client.get('/tickets/')
self.assertContains(response, 'Test')
def test_create_button_visible(self):
self.client.login(username='testuser', password='testpass')
response = self.client.get('/tickets/')
self.assertContains(response, 'Create')
def test_search_api(self):
response = self.client.get('/api/search/?q=test')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('results', data)
Accessibility¶
ARIA Attributes¶
<nav class="navbar" role="navigation" aria-label="Top navigation">
<input type="search" aria-label="Search tickets" />
<div class="dropdown">
<button
class="btn dropdown-toggle"
aria-haspopup="true"
aria-expanded="false"
aria-label="User menu"
>
{{ user.first_name }}
</button>
<div class="dropdown-menu" role="menu">
<!-- Menu items -->
</div>
</div>
</nav>
Keyboard Navigation¶
// Skip to main content link (for keyboard users)
const skipLink = document.createElement("a");
skipLink.href = "#main-content";
skipLink.textContent = "Skip to main content";
skipLink.className = "skip-to-content";
document.body.insertBefore(skipLink, document.body.firstChild);
Best Practices¶
✅ Keep topbar items to maximum 5-6 items
✅ Use clear icons with Bootstrap Icons
✅ Show notifications badge with count
✅ Include search for quick navigation
✅ Provide user profile dropdown
✅ Support mobile collapse
✅ Use accessible dropdowns
✅ Include logout option
❌ Don't overcrowd with too many items
❌ Don't hide critical actions
❌ Don't make topbar too tall
❌ Don't break keyboard navigation
Resources¶
Implementation¶
See Partials Guide for details.