Sidebars are the backbone of admin dashboards. This is how I build them in Angular 21 with Bootstrap 5 — collapsible, route-aware and mobile-friendly.

Component Structure

ng generate component layout/sidebar --standalone

Template

<!-- sidebar.component.html -->
<aside class="sidebar d-flex flex-column border-end bg-white"
  [class.collapsed]="isCollapsed"
  [class.d-none]="isMobileHidden">

  <!-- Header -->
  <div class="sidebar-header d-flex align-items-center justify-content-between px-3 py-3 border-bottom">
    <a routerLink="/" class="sidebar-brand fw-bold text-decoration-none"
      style="color:#fd4766;" *ngIf="!isCollapsed">
      BootstrapPlanet
    </a>
    <button class="btn btn-sm btn-outline-secondary border-0 ms-auto"
      (click)="toggleCollapse()" aria-label="Toggle sidebar">
      <span *ngIf="!isCollapsed">◀</span>
      <span *ngIf="isCollapsed">▶</span>
    </button>
  </div>

  <!-- Navigation -->
  <nav class="sidebar-nav flex-grow-1 py-2 overflow-y-auto">
    <ul class="nav flex-column">

      <li class="nav-item" *ngFor="let item of navItems">
        <!-- Regular item -->
        <a *ngIf="!item.children"
          class="nav-link d-flex align-items-center gap-2 px-3 py-2"
          [routerLink]="item.route"
          routerLinkActive="active"
          [routerLinkActiveOptions]="{ exact: item.exact ?? false }"
          [title]="isCollapsed ? item.label : ''">
          <span class="nav-icon flex-shrink-0">{{ item.icon }}</span>
          <span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
          <span class="badge ms-auto" *ngIf="item.badge && !isCollapsed"
            [class]="'bg-' + item.badgeColor">{{ item.badge }}</span>
        </a>

        <!-- Item with children -->
        <div *ngIf="item.children">
          <button class="nav-link d-flex align-items-center gap-2 px-3 py-2 w-100 text-start border-0 bg-transparent"
            (click)="toggleSection(item)"
            [class.active]="isSectionActive(item)"
            [title]="isCollapsed ? item.label : ''">
            <span class="nav-icon flex-shrink-0">{{ item.icon }}</span>
            <span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
            <span class="ms-auto small" *ngIf="!isCollapsed">{{ item.open ? '▾' : '▸' }}</span>
          </button>
          <ul class="nav flex-column ps-4" *ngIf="item.open && !isCollapsed">
            <li *ngFor="let child of item.children">
              <a class="nav-link py-1 small"
                [routerLink]="child.route"
                routerLinkActive="active">
                {{ child.label }}
              </a>
            </li>
          </ul>
        </div>
      </li>

    </ul>
  </nav>

  <!-- Footer -->
  <div class="sidebar-footer border-top p-3" *ngIf="!isCollapsed">
    <div class="d-flex align-items-center gap-2">
      <div class="rounded-circle d-flex align-items-center justify-content-center text-white fw-bold flex-shrink-0"
        style="width:32px;height:32px;background:#fd4766;font-size:0.75rem;">GS</div>
      <div>
        <div class="fw-semibold small">Gagan Singh</div>
        <div class="text-muted" style="font-size:0.72rem;">Founder</div>
      </div>
    </div>
  </div>

</aside>

TypeScript

// sidebar.component.ts
import { Component, OnInit } from '@angular/core'
import { RouterLink, RouterLinkActive, Router, NavigationEnd } from '@angular/router'
import { CommonModule } from '@angular/common'
import { filter } from 'rxjs/operators'

interface NavItem {
  label: string
  icon: string
  route?: string
  exact?: boolean
  badge?: string | number
  badgeColor?: string
  children?: { label: string; route: string }[]
  open?: boolean
}

@Component({
  selector: 'app-sidebar',
  standalone: true,
  imports: [RouterLink, RouterLinkActive, CommonModule],
  templateUrl: './sidebar.component.html',
  styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent implements OnInit {
  isCollapsed = false
  isMobileHidden = true

  navItems: NavItem[] = [
    { label: 'Dashboard', icon: '🏠', route: '/dashboard', exact: true },
    {
      label: 'Components', icon: '📦',
      children: [
        { label: 'Buttons', route: '/components/buttons' },
        { label: 'Cards', route: '/components/cards' },
        { label: 'Tables', route: '/components/tables' },
      ]
    },
    { label: 'Templates', icon: '🎨', route: '/templates', badge: 'New', badgeColor: 'danger' },
    { label: 'Tutorials', icon: '📚', route: '/tutorials' },
    { label: 'Analytics', icon: '📊', route: '/analytics' },
    { label: 'Settings', icon: '⚙️', route: '/settings' },
  ]

  constructor(private router: Router) {}

  ngOnInit() {
    // Restore saved state
    const saved = localStorage.getItem('sidebarCollapsed')
    if (saved !== null) this.isCollapsed = JSON.parse(saved)

    // Close on mobile after navigation
    this.router.events
      .pipe(filter(e => e instanceof NavigationEnd))
      .subscribe(() => {
        if (window.innerWidth < 992) this.isMobileHidden = true
      })
  }

  toggleCollapse() {
    this.isCollapsed = !this.isCollapsed
    localStorage.setItem('sidebarCollapsed', JSON.stringify(this.isCollapsed))
  }

  toggleSection(item: NavItem) {
    item.open = !item.open
  }

  isSectionActive(item: NavItem): boolean {
    if (!item.children) return false
    return item.children.some(child => this.router.url.startsWith(child.route))
  }

  // Called from parent layout component
  toggleMobile() {
    this.isMobileHidden = !this.isMobileHidden
  }
}

Styles

// sidebar.component.scss
.sidebar {
  width: 240px;
  min-height: 100vh;
  transition: width 0.25s ease;
  flex-shrink: 0;

  &.collapsed {
    width: 64px;

    .nav-label,
    .sidebar-brand,
    .badge,
    .sidebar-footer { display: none !important; }
  }
}

.nav-link {
  color: #6b7280;
  border-radius: 6px;
  margin: 1px 8px;
  transition: background 0.15s, color 0.15s;
  font-size: 0.88rem;

  &:hover { background: #f9fafb; color: #111; }

  &.active {
    background: rgba(253, 71, 102, 0.08);
    color: #fd4766;
    font-weight: 600;
  }
}

.nav-icon { font-size: 1rem; width: 20px; text-align: center; }

Using in Layout

<!-- app-layout.component.html -->
<div class="d-flex" style="min-height:100vh;">
  <app-sidebar #sidebar />
  <main class="flex-grow-1 overflow-auto">
    <app-navbar (menuClick)="sidebar.toggleMobile()" />
    <div class="p-4">
      <router-outlet />
    </div>
  </main>
</div>

The sidebar and navbar communicate through a simple ViewChild reference — no shared service needed for this pattern.

Frequently Asked Questions

Track a boolean isCollapsed in the component. Use [class.collapsed]='isCollapsed' on the sidebar element and control the width with CSS: .sidebar { width: 240px; transition: width 0.3s; } .sidebar.collapsed { width: 64px; }. Toggle with a button that sets isCollapsed = !isCollapsed.
Use localStorage: in the toggle method save the state localStorage.setItem('sidebarCollapsed', JSON.stringify(this.isCollapsed)). In ngOnInit load it: this.isCollapsed = JSON.parse(localStorage.getItem('sidebarCollapsed') || 'false').
Use routerLinkActive='active' directive on each menu item link. For nested routes use routerLinkActive with [routerLinkActiveOptions]='{exact: false}' so parent sections highlight when a child route is active.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.