Skip to content

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

<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

<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.