Crafting a Resizable Sidenav in Angular

A Step-by-Step Guide to Building a Flexible Sidenav Component

Aziz Nal
ITNEXT

--

TL;DR
We create a handle component and stylize it, then make the sidenav width change when the user clicks and moves the handle.

(link to finished implementation)

Introduction

In the previous article, we created a basic neat little sidenav ready for adding any other features we need.

In this article, we add a resize feature where the user can grab the sidenav by its right border and change its size. We won’t be installing any libraries or anything of the sort. It’s all Angular baby!

We’ll continue right where we left off with the base sidenav. If you missed that one, here’s a link:

Implementation

Step 1: Creating a Handle

First things first, we need a handle. We can make it as a separate element and then place it at the right border of the sidenav to make it easier to target it in event listeners.

Start by adding the following to sidenav.component.html:

<div class="resize-handle"></div>

Next, we style it.

First, there’s a border given to the :host which we want to remove since the handle is going to be replacing it and we don’t want them to overlap.

Remember:

The :host selector in Angular allows us to select the wrapping component itself, in this case, the <app-sidenav> and apply styles to it directly. (docs)

Remove the highlighted line from the styles:

:host {
// previous styles ...

background-color: rgb(237, 241, 243);

border-right: 2px solid rgb(192, 192, 192); // <- remove this line
}

The handle will be positioned absolutely and given the same height as the sidenav itself. We don’t need to add position: relative to the :host container because it’s already sticky (which we did in the previous article) which works as a reference for position: absolute.

Place these in sidenav.component.scss:

.resize-handle {
height: 100%;

background-color: rgb(165, 165, 165);
width: 2px;

position: absolute;
top: 0;
right: 0;

cursor: ew-resize;

// these prevent text selection while dragging
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;

transition: background-color 0.2s ease-out, width 0.2s ease-out;

&:hover {
width: 3px;
background-color: rgb(93, 159, 235);
}
}

You should now have something that looks like this:

Hovering on the handle should cause the handle to become blue and show a resize cursor, If you could pull off the pixel precise movements to get the cursor on the handle that is!

It shouldn’t be this difficult to catch the handle, so let’s use CSS magic to make our UX better. Add the following to the resize handle’s styles:

// ... previous styles

&:hover {
// ...
}

// buffer to make it easier to grab the resize handle
&::after {
content: "";
display: block;

height: 100%;

width: 24px;

position: absolute;
right: -12px;
z-index: 1;
}

This creates a 24px wide selection area centered on the handle. This makes it so much easier to interact with the handle!

Step 2: Adding Resizing Logic

Crack your knuckles and get ready for some coding!

We have some state to set up. Namely, we need the following:

  • max and min possible values for the sidenav width
  • a way to change the sidenav width (limited between the min and max values)
  • a way to track sidenav changes as the user interacts with the sidenav

We already have the sidenav width stored in styles.scss as a CSS variable named --sidenav-width, and we can access it as well as set the min and max values easily. Add these to your sidenav.service.ts:

export class SidenavService {
readonly sidenavMinWidth = 250;
readonly sidenavMaxWidth = window.innerWidth - 300;

get sidenavWidth(): number {
return parseInt(
getComputedStyle(document.body).getPropertyValue('--sidenav-width'),
10
);
}

setSidenavWidth(width: number) {
const clampedWidth = Math.min(
Math.max(width, this.sidenavMinWidth),
this.sidenavMaxWidth
);

document.body.style.setProperty('--sidenav-width', `${clampedWidth}px`);
}
}

Next, for tracking sidenav state changes, we can create a variable in the sidenav.component.ts file as follows:

export class SidenavComponent {
/**
* This stores the state of the resizing event and is updated
* as events are fired.
*/
resizingEvent = {
isResizing: false,
startingCursorX: 0,
startingWidth: 0,
};

constructor(public sidenavService: SidenavService) {}
}

Now it’s time for the logic. The idea is that there are three events we want to be listening for:

  1. When the mouse is down (i.e. clicked) on the handle
  2. When the mouse is being moved.
  3. When the mouse is not being held down any more.

The first event tells us the sidenav is indeed being resized. The second event tracks the changes in the sidenav as the mouse is moved and updates the width accordingly. The third event stops changes from being tracked and applied when the mouse is no longer being held down.

It’s as simple as:

  1. Start Resizing & Store Initial State (when mouse down)
  2. Calculate & Apply Changes (as mouse moves)
  3. Stop Resizing (when mouse up)

The first event listens for mousedowns on the handle which means we need to capture the mousedown event on the handle itself.

To do this, add event binding to the resize handle element in your sidenav.component.html:

<!-- previous content omitted ... -->

<div
class="resize-handle"
(mousedown)="startResizing($event)"
>
</div>

The event is bound to a method that’s not yet created, so don’t worry about any errors the editor is throwing at you.

Now we add a method to handle mousedowns in sidenav.component.ts:

export class SidenavComponent {
// ...

constructor(public sidenavService: SidenavService) {}

startResizing(event: MouseEvent): void {
this.resizingEvent = {
isResizing: true,
startingCursorX: event.clientX,
startingWidth: this.sidenavService.sidenavWidth,
};
}
}

In a nutshell, this event captures the user’s click and records the cursor position and initial sidenav width, and also sets the isResizing variable to true which enables the next event handler to function.

With the first event listener out of the way, let’s implement the second one.

Using Angular’s HostListener, we can handle all of this event in the controller. Below the startResizing method, add the following to sidenav.component.ts:

/*
* This method runs when the mouse is moved anywhere in the browser
*/
@HostListener('window:mousemove', ['$event'])
updateSidenavWidth(event: MouseEvent) {
// No need to even continue if we're not resizing
if (!this.resizingEvent.isResizing) {
return;
}

// 1. Calculate how much mouse has moved on the x-axis
const cursorDeltaX = event.clientX - this.resizingEvent.startingCursorX;

// 2. Calculate the new width according to initial width and mouse movement
const newWidth = this.resizingEvent.startingWidth + cursorDeltaX;

// 3. Set the new width
this.sidenavService.setSidenavWidth(newWidth);
}

The second event calculates and sets sizes only if the first event listener has caught the click. It knows where the cursor was when the user clicked down on the handle, and where the cursor currently is. It adds the difference to the sidenav width and badabing badaboom your sidenav is resized 🪄.

The final event stops the second event from working since the mouse is not being held down any more. It’s as simple as they come. We can place the following code under the updateSidenavWidth method:

@HostListener('window:mouseup')
stopResizing() {
this.resizingEvent.isResizing = false;
}

The third event has one job: listen to the window and set tracking to false when the mouse is up. This way, the second event listener’s code doesn’t run anymore.

We now have a functioning resizable sidenav. Nice work getting this far!

Step 3: Polish (not the language)

We made it work, now let’s make it pretty ✨

First thing to polish would be the flicker in the cursor when resizing the sidenav too quickly.

It’s a bit difficult to see in the gif, but if you try it yourself, you’ll see that the cursor keeps rapidly changing to a normal cursor then a resize cursor. This is because we are technically not hovering on the handle for very brief amounts of time as the calculations are catching up.

To fix this, we’ll do some CSS magic to increase the handle’s size only while we’re resizing. We’ll use a pseudo-element with no background so the user will still see nothing.

Here’s how we can implement it. Update the ::after pseudo-element’s styles in sidenav.component.scss like this:

.resize-handle {
// ...

&:hover {
// ...
}

&::after {
// ...
}

&::after {
// ...
}

&.resizing::after {
width: 100px;
right: calc(-100px / 2);
}
}

This makes the pseudo element width larger but only when the handle has the resizing class.

So now, we have to give the handle this class while resizing the sidenav. This is pretty simple, and can be done by adding the following to the handle’s template in sidenav.component.html:

<!-- Previous content omitted ... -->

<div
class="resize-handle"
(mousedown)="startResizing($event)"
[class.resizing]="resizingEvent.isResizing"
>
</div>

The class.resizing binding adds the resizing class when the statement within quotes is true and vice-versa, giving us exactly what we needed.

You can visualize what just happened by adding a background color to the ::after pseudo element:

The red area is our pseudo element. Notice how the cursor never leaves this area.

The handle’s cursor should now be consistently a resize cursor while we resize the sidenav (unless you go ridiculously fast)

Conclusion

In this article, we’ve demonstrated how to implement a resizable sidenav in Angular by creating a handle component, adding resizing logic, and polishing the final result.

With this guide, you should be able to create your own flexible sidenav component that can be resized according to user preferences. Give it a try and enhance the user experience of your web applications!

References

  • This codepen helped a lot.
  • Angular Host binding docs (link)
  • Angular Host listener docs (link)
  • Finished implementation (link)

--

--

Writer for

Full Stack Web Developer. Very passionate about software engineering and architecture. Also learning human languages!