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:
CSS pseudo-class
:target
CSS Grid
CSS transforms
CSS Media Queries for User Scope and Preference
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 id
that 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
visibility
to 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 10vw
to 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 visibility
which 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?