Creating the Sidenav component

Greetings. I present to your attention the translation of the article “Building a sidenav component»Published on January 21, 2021 by Adam Argyle

In this article, I want to share one way to create a responsive sidenav that supports keyboard control, works with or without JavaScript, and is supported by all browsers. You can watch the demo here

If you prefer video, below is a YouTube video for this article:

Overview

Building responsive navigation isn’t easy. Some users can use the keyboard, some will use a powerful computer to enter the site, others will use a small mobile device. But each of the visitors should be able to open and close the menu.

Demonstration of responsive layout on desktop and mobile:

Light and dark theme for iOS and Android

Approaches

While researching this component, I have combined several concepts of web development:

  1. CSS pseudo-class :target

  2. CSS Grid

  3. CSS transforms

  4. CSS Media Queries for User Scope and Preference

  5. JS to improve usability

In my solution on large screens, the sidebar is static, and it becomes “sliding” only when the width of the viewport is smaller 540px… This size will be a reference point for switching between interactive layout for mobile and static for desktops.

CSS pseudo-class: target

Link <a>, which opens the panel, sets the URL hash to #sidenav-open… The sidebar element itself has idthat matches this value. The closing link sets the URL hash to an empty value (''):

<a href="#sidenav-open" id="sidenav-button" title="Open Menu" aria-label="Open Menu">

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<aside id="sidenav-open">
  …
</aside>

Clicking on these links changes the state (show or hide) of the sidebar depending on the URL in the address bar:

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
  }

  #sidenav-open:target {
    visibility: visible;
  }
}

CSS Grid

Previously, I only used absolute or fixed positioning for the sidebar component. CSS Grid technology with its syntax grid-area, opens up another way, allowing us to assign multiple elements to one row or column.

Stacks

The main element of the layout #sidenav-container is a grid element that creates 1 row and 2 columns, and the first cell is named stack… When space is limited, CSS assigns all elements that are children of <main> into the same grid area, placing all items in one cell in a stack.

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;
}

@media (max-width: 540px) {
  #sidenav-container > * {
    grid-area: stack;
  }
}

Substrate

<aside> Is an animated element that contains side navigation. It has two children: a container <nav>given the name [nav] and substrate <a> With name [escape]which is used to close the menu.

#sidenav-open {
  display: grid;
  grid-template-columns: [nav] 2fr [escape] 1fr;
}

By changing values 2fr & 1fr you can adjust the ratio between the panel and the remaining space while the side menu is open.

Demonstration of the result of resizing the panel

CSS transforms and transitions

Now our layout fits into the small field of view of a mobile device. So far, the sidebar is overlaid on the main content by default. Here’s the functionality I want to expand on in the next section:

  • Animated opening and closing

  • Animation only if the user does not prefer to disable it

  • Animation visibilityto keep the keyboard focus on the screen

Since it came to implementing animated motion, first of all I would like to start with accessibility

Available animation

Not everyone wants to see the slide-out animation. In our example, the value of the CSS variable depends on the user’s preference. --duration that determines the duration of the animation. This variable is located inside the media query, which takes into account the settings of the user’s operating system.

#sidenav-open {
  --duration: .6s;
}

@media (prefers-reduced-motion: reduce) {
  #sidenav-open {
    --duration: 1ms;
  }
}

Demonstration of the interface with different animation settings

Now, if the user prefers a reduced amount of animation, the panel will appear instantly.

Transition, transformation, displacement

Hidden panel (default)

To keep the panel off-screen on mobiles by default, I offset it with transform: translateX(-110vw).

Note that in addition to the normal width of the scope -100vw I additionally added 10vwto make sure that the sidebar shadow is not visible on the screen when hidden.

@media (max-width: 540px) {
  #sidenav-open {
    visibility: hidden;
    transform: translateX(-110vw);
    will-change: transform;
    transition:
      transform var(--duration) var(--easeOutExpo),
      visibility 0s linear var(--duration);
  }
}

Open panel

When the item #sidenav matches the pseudo-class :target, set positioning with translateX() to the standard value 0 and see how the CSS over the time set in the variable var(--duration), will displace the element from its original “hidden” position equal to -110vw to the “open” position equal to 0

@media (max-width: 540px) {
  #sidenav-open:target {
    visibility: visible;
    transform: translateX(0);
    transition:
      transform var(--duration) var(--easeOutExpo);
  }
}

Transition for the visibility property

Now, when the panel is out of scope, it needs to be hidden from screen readers, so that they do not transfer focus to its elements. I implemented this with a transition for the property visibilitywhich is executed when changing the pseudo-class :target

  • When opening the transition, you do not need to apply so that the immediately visible panel slides out of the screen

  • When closing the panel for a property visibility you need to apply the transition, but with a delay so that it becomes invisible only after the transition is completed

Accessibility improvements

Links

The above solution relies on URL changes to manage the state of the panel. Naturally, here you need to use the element <a>which has some advantages in terms of accessibility. Let’s complement our interactive elements with accessible captions that reflect their purpose.

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">
  <svg>...</svg>
</a>

Demonstration of interaction using the keyboard and screen reader

Our main interaction buttons now have a meaningful label for both mouse and keyboard users.

: is (: hover,: focus)

This handy pseudo-class allows us to style the states at the same time hover and focus

.hamburger:is(:hover, :focus) svg > line {
  stroke: hsl(var(--brandHSL));
}

Adding JavaScript

Escape to close

Button Escape on the keyboard should close the menu, right? Let’s make this opportunity happen

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', event => {
  if (event.code === 'Escape') document.location.hash="";
});

Browser history

To prevent each opening and closing of the panel from creating a separate entry in the browser history, add the following code for the close button

<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu" onchange="history.go(-1)"></a>

When the panel is closed, the history entry will be deleted, as if the panel had never been opened.

Focus

The following code snippet helps us to place focus on the open and close buttons on the corresponding panel action. I want to make switching easy

sidenav.addEventListener('transitionend', e => {
  const isOpen = document.location.hash === '#sidenav-open';

  isOpen
      ? document.querySelector('#sidenav-close').focus()
      : document.querySelector('#sidenav-button').focus();
})

When the sidebar opens, focus is on the close button. When the panel is closed, the focus is on the open button. I am doing this with JavaScript, calling on an element focus()

Conclusion

Now you know about my approach to the implementation of this component. How would you implement it?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *