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.