This content originally appeared on web.dev and was authored by Adam Argyle
In this post I want to share with you how I prototyped a Sidenav component for the web that is responsive, stateful, supports keyboard navigation, works with and without Javascript, and works across browsers. Try the demo.
If you prefer video, here's a YouTube version of this post:
Overview
It's tough building a responsive navigation system. Some users will be on a keyboard, some will have powerful desktops, and some will visit from a small mobile device. Everyone visiting should be able to open and close the menu.
Web Tactics
In this component exploration I had the joy of combining a few critical web platform features:
- CSS
:target
- CSS grid
- CSS transforms
- CSS Media Queries for viewport and user preference
- JS for
focus
UX enhancements
My solution has one sidebar and toggles only when at a "mobile" viewport of 540px
or less.
540px
will be our breakpoint for switching between the mobile interactive layout and the static desktop layout.
CSS :target
pseudo-class
One <a>
link sets the url hash to #sidenav-open
and the other to empty (''
).
Lastly, an element has the id
to match the hash:
<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 each of these links changes the hash state of our page URL, then with a pseudo-class I show and hide the sidenav:
@media (max-width: 540px) {
#sidenav-open {
visibility: hidden;
}
#sidenav-open:target {
visibility: visible;
}
}
CSS Grid
In the past, I only used absolute or fixed position
sidenav layouts and components. Grid though, with its grid-area
syntax,
lets us assign multiple elements to the same row or column.
Stacks
The primary layout element #sidenav-container
is a grid that creates 1 row and 2 columns,
1 of each are named stack
. When space is constrained, CSS assigns all of the <main>
element's
children to the same grid name, placing all elements into the same space, creating 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;
}
}
Menu backdrop
The <aside>
is the animating element that contains the side navigation. It has
2 children: the navigation container <nav>
named [nav]
and a backdrop <a>
named [escape]
, which is used to close the menu.
#sidenav-open {
display: grid;
grid-template-columns: [nav] 2fr [escape] 1fr;
}
Adjust 2fr
& 1fr
to find the ratio you like for the menu overlay and its negative space close button.
CSS 3D transforms & transitions
Our layout is now stacked at a mobile viewport size. Until I add some new styles, it's overlaying our article by default. Here's some UX I'm shooting for in this next section:
- Animate open and close
- Only animate with motion if the user is OK with that
- Animate
visibility
so keyboard focus doesn't enter the offscreen element
As I begin to implement motion animations, I want to start with accessibility top of mind.
Accessible motion
Not everyone will want a slide out motion experience. In our solution this preference
is applied by adjusting a --duration
CSS variable inside a media query. This media query value represents
a user's operating system preference for motion (if available).
#sidenav-open {
--duration: .6s;
}
@media (prefers-reduced-motion: reduce) {
#sidenav-open {
--duration: 1ms;
}
}
Now when our sidenav is sliding open and closed, if a user prefers reduced motion, I instantly move the element into view, maintaining state without motion.
Transition, transform, translate
Sidenav out (default)
To set the default state of our sidenav on mobile to an offscreen state,
I position the element with transform: translateX(-110vw)
.
Note, I added another 10vw
to the typical offscreen code of -100vw
,
to ensure the box-shadow
of the sidenav doesn't peek into the main viewport when it's 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);
}
}
Sidenav in
When the #sidenav
element matches as :target
, set the translateX()
position to homebase 0
,
and watch as CSS slides the element from its out position of -110vw
, to its "in"
position of 0
over var(--duration)
when the URL hash is changed.
@media (max-width: 540px) {
#sidenav-open:target {
visibility: visible;
transform: translateX(0);
transition:
transform var(--duration) var(--easeOutExpo);
}
}
Transition visibility
The goal now is to hide the menu from screenreaders when it's out,
so systems don't put focus into an offscreen menu. I accomplish this by setting a
visibility transition when the :target
changes.
- When going in, don't transition visibility; be visible right away so I can see the element slide in and accept focus.
- When going out, transition visibility but delay it, so it flips to
hidden
at the end of the transition out.
Accessibility UX enhancements
Links
This solution relies on changing the URL in order for the state to be managed.
Naturally, the <a>
element should be used here, and it gets some nice accessibility
features for free. Let's adorn our interactive elements with labels clearly articulating intent.
<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>
Now our primary interaction buttons clearly state their intent for both mouse and keyboard.
:is(:hover, :focus)
This handy CSS functional pseudo-selector lets us swiftly be inclusive with our hover styles by sharing them with focus as well.
.hamburger:is(:hover, :focus) svg > line {
stroke: hsl(var(--brandHSL));
}
Sprinkle on Javascript
Press escape
to close
The Escape
key on your keyboard should close the menu right? Let's wire that up.
const sidenav = document.querySelector('#sidenav-open');
sidenav.addEventListener('keyup', event => {
if (event.code === 'Escape') document.location.hash = '';
});
Focus UX
The next snippet helps us put focus on the open and close buttons after they open or close. I want to make toggling 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 sidenav opens, focus the close button. When the sidenav closes,
focus the open button. I do this by calling focus()
on the element in JavaScript.
Conclusion
Now that you know how I did it, how would you?! This makes for some fun component architecture! Who's going to make the 1st version with slots? ?
Let's diversify our approaches and learn all the ways to build on the web. Create a Glitch, tweet me your version, and I'll add it to the Community remixes section below.
Community remixes
- @_developit with custom elements: demo & code
- @mayeedwin1 with HTML/CSS/JS: demo & code
- @a_nurella with a Glitch Remix: demo & code
- @EvroMalarkey with HTML/CSS/JS: demo & code
This content originally appeared on web.dev and was authored by Adam Argyle
Adam Argyle | Sciencx (2021-01-21T00:00:00+00:00) Building a sidenav component. Retrieved from https://www.scien.cx/2021/01/21/building-a-sidenav-component/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.