Salesforce, Python, SQL, & other ways to put your data where you need it

Need event music? 🎸

Live and recorded jazz, pop, and meditative music for your virtual conference / Zoom wedding / yoga class / private party with quality sound and a smooth technical experience

Responsive accessible navbar

12 Nov 2020 🔖 accessibility jamstack web development
💬 EN

Last night I improved accessibility a bit for the Katie Kodes navigation menu.

  1. I wrote the word “menu” before the “hamburger” icon on small screens.
  2. I moved the “hamburger” before the navigation links so that when you press “enter” to expand the navigation menu (to do: add some screen-reader text clarifying that that’s how it works), pressing “tab” continues to take you to the links, rather than into the main page.
  3. I fixed my CSS so that if you tab through the navbar, on a big screen or small, menu items with drop-downs actually drop down and let you tab through their options (awkwardly, if you have to shift-tab after you’ve left a parent drop-down, you kind of have to go back to the parent’s predecessor and then re-tab into the parent to get the dropdown to appear again, but it’s better than not working at all).
  4. I switched the wrapper HTML element from a div to a nav, added a role, and added an aria-label.

Otherwise, I was pleasantly surprised how decently engineered the navbar I spent 2 weeks building when I started the blog was. a elements are used for links; button elements are mostly used for interaction controls.

Improvements for the wishlist are:

  1. Keyboard navigation could be even a little bit cleaner – as always, Real Python’s doing a great job.
    • (I’ll have to dig into whether there’s something I don’t know about their use of # a tags instead of buttons for dropdown labels.)
  2. Switching on Windows Narrator (Win + Ctrl + Enter for on/off), I learned that visitors hear “Salesforce, Python … & French” twice for every page. I’d like to fix that.
  3. I’d like to slow down the animation on the drop-down, to give people with mobility issues a little more time to click sublinks.
  4. I should switch the “hamburger” from a link to a button.
  5. I should add arrows & screen reader hints to the labels of drop-down items to indicate more clearly that they reveal submenus.
  6. Not a11y, just an attractiveness to-do item: I realized I didn’t optimize my nav wrapping for small screens but not small enough to hit my small-screen breakpoint. Now that I added a “shop” option, my navbar looks silly at medium screen sizes. Right-align it, at the very least.
  7. My dropdowns work without JS turned on, but my hamburger doesn’t. (Is this still a thing?)
    • Okay, so … it looks like I have this as my CSS for small screens. I could probably just … use JS or Noscript to put a class into the navbar, then use that to decide NOT to go into display: none mode. Or, heck, just let the whole navbar always be out, no hamburger. Looks like a common practice is to add a “noscript” class to your HTML tag, then in the HEAD, before loading any CSS, run a small JS snippet to remove it (or flip it to “yesscript”). All CSS responsible for hiding things “unless exposed by JS click” is trained to only be relevant if “noscript” is not present or maybe if “yesscript” is present.
       @media screen and (max-width: 767px) {
        .topnav.whole-bar{
            & a:not(.topnav__home), .topnav__dropdown {
                display: none;
            }
        }
        &.expandedhamburger {
            position: relative;
            .topnav__dropdown__menuitems {
                position: relative;
            }
            .topnav__dropdown, .tn-dropbtn {
                text-align: left;
                display: block; // Expose things once hamburger toggled open
                width: 100%;
            }
            & a {
                text-align: left;
                &:not(.topnav__hamburger) {
                    display: block; // Expose things once hamburger toggled open
                    float: none;
                }
                &.topnav__hamburger { // Style the hamburger A on small screens when class "expandedhamburger" is toggled
                    position: absolute;
                    right: 0;
                    top: 0;
                }
            }
        }
       }
      
    • My year-by-year archive also doesn’t work w/o JS turned on. Maybe I should add a “noscript” that leads to some sort of full archive page.

Here’s the overall structure of the pattern for rendering my HTML, expressed here in the Shopify Liquid templating language:

{%- comment -%}Be sure to use hyphens in Liquid tags, lest mystery-whitespace show up between the LI's:  https://shopify.github.io/liquid/basics/whitespace/{%- endcomment -%}
<nav role="navigation" aria-label="Main Navigation" id="topnav" class="topnav whole-bar">
 <a class="topnav__home" href="{{ site.url }}" title="Home"><img src="{{ site.imgpath }}" class="tn-cornerlogo" alt="{{ site.title }}"></a>
 <a href="javascript:void(0);" class="topnav__hamburger" onclick="flyHamburgerOutIn()">Menu &#9776;</a>
 {%- for link in site.mainNavLinks -%}
  {%- if link.sublinks == blank or link.sublinks.size == 0 -%}
   <a href="{{ link.url }}">{{ link.text }}</a>
  {%- else -%}
   <div class="topnav__dropdown">
    <button class="topnav__dropdown__button" tabindex="0">{{ link.text }}</button>
    <div class="topnav__dropdown__menuitems">
     {%- for sublink in link.sublinks -%}
      <a href="{{ sublink.url }}">{{ sublink.text }}</a>
     {%- endfor -%}
    </div>
   </div>
  {%- endif -%}
 {%- endfor -%}
</nav>

There’s not a lot of JavaScript, but there’s flyHamburgerOutIn() attached to onClick for the “hamburger” control on small screens:

function flyHamburgerOutIn() {
  var x = document.getElementById("topnav");
  x.classList.toggle("expandedhamburger");
}

The rest of the magic is done with CSS.

  • TO DO: paste my CSS

FWIW, Real Python’s HTML looks more like this (expressed in the Liquid templating language):

<nav class="navbar fixed-top navbar-expand-lg navbar-dark flex-column ">
 <div class="container flex-row">
  <a class="navbar-brand" href="/"><img src="{{ site.imgpath }}" class="d-inline-block align-top" alt="{{ site.title }}" /></a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">Menu &#9776;</button>
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
   <ul class="navbar-nav mr-2 flex-fill">
   {%- for link in site.mainNavLinks -%}
    {%- if link.sublinks == blank or link.sublinks.size == 0 -%}
    <li class="nav-item">
     <a class="nav-link" href="{{ link.url }}">{{ link.text }}</a>
    </li>
    {%- else -%}
    <li class="nav-item dropdown">
     <a class="nav-link dropdown-toggle" href="#" id="{{ link.ariaDropdownLabel }}" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ link.text }}</a>
     <div class="dropdown-menu" aria-labelledby="{{ link.ariaDropdownLabel }}">
      {%- for sublink in link.sublinks -%}
       {%- comment -%}2nd-to-last link in the list also has an "pb-3" class{%- endcomment -%}
       {%- comment -%}Last link in the list also has an "border-top" class{%- endcomment -%}
       <a class="dropdown-item" href="{{ sublink.url }}">{{ sublink.text }}</a>
      {%- endfor -%}
     </div>{%- comment -%}End div.dropdown-menu%labelledbyariaX{%- endcomment -%}
    </li>{%- comment -%}End li.nav-item.dropdown{%- endcomment -%}
    {%- endif -%}
   {%- endfor -%}
   </ul>
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}-BEGIN SECTION NOT IN MY NAVBAR-{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}A plain old link to a dedicated search page; visible on small screen; hidden on big screen{%- endcomment -%}
   <div class="d-block d-xl-none">
    <ul class="navbar-nav">
     <li class="nav-item">
      <a class="nav-link" href="/search" title="Search"><span class="d-block d-lg-none"><i class="fa fa-search"></i> Search</span><span class="d-none d-lg-block"><i class="fa fa-search"></i></span></a>
     </li>
    </ul>
   </div>
   {%- comment -%}An inline search form; hidden on small screen; visible on big screen{%- endcomment -%}
   <div class="d-none d-xl-flex align-items-center mr-2">
    <form class="form-inline" action="/search" method="GET">
     <a class="position-absolute" href="/search" title="Search"><i class="fa fa-search fa-fw text-muted pl-2"></i></a>
     <input class="search-field form-control form-control-md mr-sm-1 mr-lg-2 w-100" style="padding-left: 2rem;" maxlength="50" type="search" placeholder="Search" aria-label="Search" name="q">
     <input type="hidden" name="_from" value="nav">
    </form>
   </div>
   {%- comment -%}Another link list, but the styling is kind of special on its 2 members; possibly not part of the CMS{%- endcomment -%}
   <ul class="navbar-nav">
    <li class="nav-item form-inline">
     <a class="ml-2 ml-lg-0 btn btn-sm btn-primary px-3" href="/account/join/">Join</a>
    </li>
    <li class="nav-item">
     <a class="btn text-light" href="/account/login/">Sign‑In</a>
    </li>
   </ul>
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}-END SECTION NOT IN MY NAVBAR-{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
   {%- comment -%}---{%- endcomment -%}
  </div> <!-- End div.navbarSupportedContent -->
 </div>
</nav>

Like my original navbar, their “flyouts” are simple back-to-back a tags wrapped in a div. They don’t mess with nested ul and li’s.

However, unlike my original navbar, they do wrap top-level a tags in li’s. Not sure if I need to imitate that or not to get the full “pleasant to navigate” experience.

Also, Real Python’s “hamburger” & “drop-down” controls don’t do anything if you have JavaScript disabled. Not sure how much one is expected to code for that these days.

--- ---