Table of Contents
- Prequisites
- Step 1: Setup Angular
- Step 2: Add Angular Material
- Step 3: Import required components
- Step 4: Implement the components
- Step 5: Adding responsiveness
- Step 6: Toggle the menu
- Step 7: Collapsing the navigation
- Bonus: Conditional classes
Introduction
Since I am in the process of onboarding with Angular using the Angular Material framework in particular, like many developers before me, I found myself pondering how to create a responsive side navigation. This navigation should push the main content to the side on desktops, yet overlap it on mobile resolutions.
While this task seemed fairly easy, I also aimed to ensure that the sidenav wouldn't completely disappear on tablet and desktop resolutions. Instead, I wanted to display a collapsed version using only icons.
Mobile Behaviour
Desktop/Tablet Behaviour
This stretch goal was what inspired me to write this post. I wanted to share not only the solution I came up with but also because some of the highly ranked suggested solutions seemed outdated or not elegant, as they sometimes heavily rely on overwriting classes of the UI component library.
Repository / Live Demo
You can find and fork my example repository at my Github profile or try it out live on Netlify.
Prequisites
For this tutorial, you should prepare everything you need to create and develop an Angular application. For more information, please visit the Angular documentation. I also recommend having a basic understanding of modern Angular application architecture since I won't delve too deeply into the specifics of the used directives and components, but rather provide links to the documentation.
Step 1: Setup Angular
Create a new Angular application
ng new material-responsive-sidenav
While it's not particularly crucial for this demonstration, I opted to include Angular routing
and chose to use SCSS
.
Step 2: Add Angular Material
After all npm packages have been installed, we can add the Material framework to our Angular application by navigating toour newly created app directory
ng add @angular/material
After confirming, you are free to choose a preset theme or create a custom one. Since this guide does not focus on theming in Angular, I opted for a prebuilt theme. Additionally, I set up global typography styles
and included & enabled animations
.
Once the import is complete, you can start the application with
ng serve
The application will by default be available on http://localhost:4200/
Step 3: Import required components
Since we are going to use Angular components, let's clean up the app.component.html
file within the app-root directory and modify the app.module.ts
file to include the required Material components.
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {MatListModule} from '@angular/material/list';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatIconModule,
MatButtonModule,
MatToolbarModule,
MatSidenavModule,
MatListModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
What is happening here:
Since we are using components from Angulars Material framework
these imports are required:
-
Material Toolbar
for the appbar on top -
Material Icon
andMaterial Button
for the menu icon botton we are going to implement -
Material Sidnav
for our sidenav. -
Material List
to wrap our navigation items nicely.
By adding the imports in the global app.module.ts
, they will be available in every (non-standalone) component going forward without the need to import them individually. Since we won't be adding an additional component in this tutorial, I chose this approach for simplicity. However, it's worth noting that you could also create separate components for the sidenav and appbar if desired.
Step 4: Implement the components
After importing all the required Angular Material components for now, we can start building the template by editing the app.component.html file.
Adding the Toolbar
<mat-toolbar color="primary">
<button mat-icon-button aria-label="Menu icon">
<mat-icon>menu</mat-icon>
</button>
<h1>Responsive Material Sidenavigation</h1>
</mat-toolbar>
I chose to implement a simple Material toolbar with just a button for toggling our menu as well as an app title.
Adding the Sidenavigation
<mat-sidenav-container autosize>
<mat-sidenav [opened]="true" mode="side">
<mat-nav-list>
<a mat-list-item>
<span class="entry">
<mat-icon>house</mat-icon>
<span>Dashboard</span>
</span>
</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<h2>Content</h2>
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
For the Material Sidenavigation, I enabled autosize
to resize the container during toggling. This will make sure the main content is moving accordingly.
[opened]="true" and mode="side" are used as placeholders for now, as we will add the functionality moving forward. If you're curious about the square brackets, you can find more information about Angular Property Binding here.
Inside the mat-sidenav
component, I am creating a mat-nav-list
and adding the mat-list-item
property to our navigation link. This wrapping helps structure the layout and utilize the nice hover and click ripple effects when clicking. The a link itself consists of another Material Icon and a menu text.
The mat-sidenav-content
component contains a placeholder h2
and also utilizes Angular's router-outlet
in preparation for possible routing to different pages/components.
If you are coding along with me, you will notice that the result is not quite satisfying yet. Let's enhance the visual appeal by editing the app.component.scss
file.
h1 {
padding: 0 1rem;
}
h2 {padding: 1rem;}
mat-toolbar{
position:fixed;
top:0;
z-index: 2;
}
mat-sidenav-container {
height:100%;
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav {
padding-top: 3.5rem;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
}
}
// Move the content down so that it won't be hidden by the toolbar
mat-sidenav-content{
padding-top: 3.5rem;
@media screen and (min-width: 600px) {
padding-top: 4rem;
}
}
What is happening here
As I'm utilizing the default behavior of Material's toolbar
, it's important to note that its height changes based on the screen size
, whether it's wider or smaller than 600px. Therefore, to keep our appbar fixed at the top, I'm adding padding
to both the sidenav
and content
sections depending on the screen size.
At this stage, our Angular application appears like this:
Step 5: Adding responsiveness
Let's return to the sidenav
component where we previously added placeholders for the opened state as well as the mode it is using.
Since we want to change the mode based on screensize, we will replace the placeholder in the app.component.html
file and implement a BreakpointObserver
which is a handy feature of the Material Component Development Kit. To do this, we will modify the app.component.ts
file as followed:
import { BreakpointObserver } from '@angular/cdk/layout';
import {
Component,
ViewChild,
} from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'material-responsive-sidenav';
@ViewChild(MatSidenav)
sidenav!: MatSidenav;
isMobile= true;
constructor(private observer: BreakpointObserver) {}
ngOnInit() {
this.observer.observe(['(max-width: 800px)']).subscribe((screenSize) => {
if(screenSize.matches){
this.isMobile = true;
} else {
this.isMobile = false;
}
});
}
}
What is happening here:
At the very beginning, we import the BreakpointObserver
along with the ViewChild
and MatSidenav
modules, as we will need these for implementing the observer.
Within the component class, we introduce the ViewChild property and assign it to the sidenav. This step is taken to monitor the behavior of the side navigation.
Following the declaration of sidenav and the creation of a variable that indicates whether a mobile resolution is being used (we will revisit this later), we initialize the BreakpointObserver service using the constructor.
Next, we utilize the NgOnInit lifecycle hook to influence the behavior of the sidenav component and to toggle the isMobile state between true and false.
Before proceeding to the next step, we need to address the changes required in our app.component.html
file by using conditional property binding:
<mat-sidenav [mode]="isMobile ? 'over' : 'side'" [opened]="isMobile ? 'false' : 'true'">
// ...
</mat-sidenav>
Step 6: Toggle the menu
Continuing the editing of the app.component.ts
, we proceed to create a function named toggleMenu
where we will utilize the isMobile
variable.
toggleMenu() {
if(this.isMobile){
this.sidenav.toggle();
} else {
// do nothing for now
}
}
To finaly open and close the navigation on mobile resolutions, we modify the app.component.html
and add an event listener to the button in our appbar.
<button mat-icon-button aria-label="Menu icon" (click)="toggleMenu()">
<mat-icon>menu</mat-icon>
</button>
At this point, our appbar button will open and close the side navigation using the built-in method of the Material sidnav
component.
Step 7: Collapsing the navigation
The only remaining task is to add the collapsing behavior to our sidenav
component. To achieve this, we will initialize another variable in our app.component.ts
file called isCollapsed
and incorporate it into our toggle function
.
Adding the Variable
isCollapsed = true;
Complete the Toggle Function
toggleMenu() {
if(this.isMobile){
this.sidenav.toggle();
this.isCollapsed = false; // On mobile, the menu can never be collapsed
} else {
this.sidenav.open(); // On desktop/tablet, the menu can never be fully closed
this.isCollapsed = !this.isCollapsed;
}
}
To complete this step, the final task is to implement a conditional rendering in our app.component.html
template.
Add Conditional Rendering
<a mat-list-item>
<span class="entry">
<mat-icon>house</mat-icon>
<span *ngIf="!isCollapsed">Dashboard</span>
</span>
</a>
What is happening here:
We have successfully completed the implementation of interactivity for our responsive menu. The decision was made to enable toggling only on mobile screens. On larger resolutions, we ensure that the menu remains open, but we instead toggle a variable that is utilized to hide the text, displaying only the icon.
Bonus Step: Conditional classes
In case you want to implement a specific styling based on wether the side navigation is collapsed or expaned in bigger resolutions, with ngClass you can take advantage of another built-in directive of Angular to apply different styles, e.g. a specific width.
Apply ngClass directive
<mat-sidenav [ngClass]="!isCollapsed ? 'expanded' : ''" [mode]="isMobile ? 'over' : 'side'" [opened]="isMobile ? 'false' : 'true'">
// ...
</mat-sidenav>
Modify stylesheet
.expanded {
width: 250px;
}
Top comments (4)
Hello David, do you have a tuto for add new page routed for each list on the menu?
import {MatListModule} from '@angular/material/list'; is missing in the app.module.ts file , please update the screen shot.
Took some time, added it now, thank you :)
Thanks a lot for the post it was very helpful