This project gives you an application template that includes a responsive, multi-level Angular Material menu.
You can download the code or see the code at GitHub.
This project was built using the Starter Application HERE as the starting point.
Layout of the Application
The layout of the header, main content, and the footer is in app.component.html:
1 2 3 4 5 6 7 8 |
<!-- main layout --> <div id="body" class="mat-typography"> <header></header> <main> <router-outlet></router-outlet> </main> <footer></footer> </div> |
The layout is just a CSS grid defined in app.component.scss:
1 2 3 4 5 |
#body { display: grid; grid: min-content auto min-content / 1fr; height: 100%; } |
The <main> element is the center part that contains the menu and the content, it routes to main.component.html, which has the mat-sidenav-container that has the menu on the left and the content on the right:
1 2 3 4 5 6 7 8 |
<mat-sidenav-container> <mat-sidenav #sidenav [opened]="showSideNav" [mode]="showSideNav ? 'side' : 'over'"> ... </mat-sidenav> <mat-sidenav-content> <router-outlet></router-outlet> </mat-sidenav-content> </mat-sidenav-container> |
Adding Responsiveness
We want to show the menu when the screen width is above 768px, and hide the menu if otherwise. This can be done by using the BreakpointObserver to detect the screen size:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; @Component({ templateUrl: './main.component.html', styleUrls: ['./main.component.scss'] }) export class MainComponent implements OnInit { showSideNav: boolean = false; constructor(private breakPointObserver: BreakpointObserver) { } ngOnInit(): void { this.breakPointObserver .observe(['(min-width: 768px)']) .subscribe((state: BreakpointState) => { this.showSideNav = state.matches; }); } } |
When the showSideNav flag is true, we show the menu by setting [opened] to true and [mode] to ‘side’. Otherwise [opened] is false and [mode] is ‘over’:
1 |
<mat-sidenav #sidenav [opened]="showSideNav" [mode]="showSideNav ? 'side' : 'over'"> |
When the screen size is under 768px, we need to provide a button that the user can click on to open the menu. The button is declared in header.component.html:
1 2 3 |
<button mat-button class="menu-button" (click)="toggleMenu()"> <mat-icon>menu</mat-icon> </button> |
We hide the button if the screen width exceeds 768px, which is the $screen-size-mid sass variable:
1 2 3 4 5 |
.menu-button{ @media (min-width: $screen-size-mid){ display: none; } } |
When the button is clicked, we call the service method menuService.toggleMenu():
1 2 3 4 5 6 7 8 |
export class HeaderComponent implements OnInit { constructor(private menuService: MenuService) { } toggleMenu(){ this.menuService.toggleMenu(); } } |
The MenuService use the rxjs Subject without any return type since it just need to detect when the button is clicked:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Subject, Observable } from 'rxjs'; export class MenuService { private menuToggle = new Subject(); menuToggle$ = this.menuToggle.asObservable(); constructor() { } //toggles menu when called toggleMenu(): Observable<unknown>{ this.menuToggle.next(); return this.menuToggle$; } } |
The main.component detects when the menu should appear by subscribing to the service. When the event is received, it calls the toggle() method of the MatSidenav which shows or hides the menu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export class MainComponent implements OnInit { @ViewChild('sidenav') sidenav: MatSidenav; constructor(private breakPointObserver: BreakpointObserver, private menuService: MenuService, private router: Router) { } ngOnInit(): void { this.menuService.menuToggle$.subscribe(()=>{ this.sidenav.toggle(); }); } } |
Selecting Menu Item
When a menu item is clicked, we want to 1) hide the menu if the screen width is smaller than 768px, and 2) give visual indication on which menu item was clicked.
To hide the menu if the screen is small, we can just call the service to toggle the menu:
1 2 3 4 |
toggleMenu(){ if (!this.showSideNav) //if small screen this.menuService.toggleMenu(); } |
To give visual indication on which menu item was clicked, we use the variable selectedMenuItem to track and change the css class of the menu item:
1 2 3 |
customerClick(){ this.selectedMenuItem = 'customer'; } |
1 |
<mat-list-item (click)="customerClick()" [ngClass]="selectedMenuItem == 'customer' ? 'selected-menu-item': null"> |
Adding SubMenu
The subMenu can be added by adding a div and display/hide using CSS transform. For example, using MenuX as a menu item that can expand a submenu, we can declare below:
1 2 3 4 5 6 7 8 9 |
<mat-list-item (click)="showMenuX = !showMenuX"> <span>Menu X</span> <mat-icon mat-list-icon>all_inclusive</mat-icon> <mat-icon class="menu-button" [ngClass]="{'rotated' : showMenuX}">expand_more</mat-icon> </mat-list-item> <div class="submenu" [ngClass]="{'expanded' : showMenuX}"> <mat-list-item><mat-icon mat-list-icon>bedtime</mat-icon>Item 1</mat-list-item> <mat-list-item><mat-icon mat-list-icon>bolt</mat-icon>Item 2</mat-list-item> </div> |
The showMenuX is just a variable we use to track if the submenu should be displayed. Every click of the item will toggle the show or hide of the submenu.
In [ngClass] we pass in an object, which means that when the value is true, the class specified will be added. Therefore in the example above, when showMenuX is true, the ‘expanded’ class will be added to the div.
In the CSS we hide the submenu by default, and when the ‘expanded’ class is added we display the submenu by using CSS scaleY(1) transform:
1 2 3 4 5 6 7 8 9 10 11 |
.submenu { overflow-y: hidden; transition: transform 300ms; transform: scaleY(0); transform-origin: top; padding-left: 2rem; } .submenu.expanded { transform: scaleY(1); } |
And that’s all, hope you will find this project useful in building your application with responsive menu.