guide · computers

ABS Mobile Nav: The Three Rules

Mobile nav on ABS runs through a drawer (not modal), with aria-expanded state on the toggle, and focus stays trapped inside until closed. The body-scroll-lock and the close behaviors, in three rules.

July 14, 2026 · By Alastair Fraser

A friendly retro robot at a three-drawer cabinet labeled DRAWER, ARIA-EXPANDED, and SCROLL-LOCK — pulling the top drawer open while a small human figure on the right tests the menu on a phone.

The three rules in one sentence

ABS mobile nav opens a drawer (not a modal), toggles aria-expanded on the button, and locks body scroll while open. Three rules that compose cleanly with the desktop layout, the book navigation menu, and the search bar.

The mobile drawer is reachable from a single hamburger button at ≤720px viewport widths. It does not collide with desktop nav, and it does not block keyboard users from completing their intent.

Rule 1: drawer-first, not modal

The drawer is a side panel that the user can dismiss. It is not a modal dialog that demands attention.

What this means:

  • The drawer slides in from the left; the rest of the page stays visible behind a translucent backdrop.
  • Tapping outside the drawer (on the backdrop) closes it. The user does not need to find the close button.
  • The drawer does NOT prevent background tab-switching by tab focus; that’s a modal pattern.
  • The drawer does NOT require the user to dismiss it before they can interact with anything else.

Failure shapes:

  • The drawer is implemented as <dialog> with showModal(). That’s a modal; user expects Esc + focus lock. Don’t.
  • The drawer is a position:fixed panel with no backdrop. Users can’t tap-out-of it; readers can’t tell when they’re “in” it.
  • The drawer covers the entire viewport with inset: 0. That’s a modal; don’t.

Rule 2: aria-expanded state on the button

The hamburger button carries aria-expanded="false" when closed and "true" when open. The drawer carries hidden (boolean HTML attribute) for the opposite state.

The button is id="menuBtn"; the drawer is id="mobileMenu". The button is always present in the DOM (CSS hides it on desktop with display:none); the drawer toggles between hidden=true and false.

<button id="menuBtn" aria-expanded="false" aria-label="Open navigation menu">
  <span aria-hidden="true">☰</span>
</button>
<nav id="mobileMenu" hidden aria-label="Mobile navigation">
  <a href="/news/">News</a>
  ...
</nav>

When opened:

menuBtn.setAttribute('aria-expanded', 'true');
menuBtn.setAttribute('aria-label', 'Close navigation menu');
menu.hidden = false;
menu.classList.add('is-open');

When closed, reverse those.

Failure shapes:

  • Toggle a class but not the hidden attribute. Screen readers still announce the drawer as available.
  • Use the inverse: aria-hidden on the drawer. That’s the wrong aria attribute; hidden is correct here.
  • Set aria-expanded on the drawer (not the button). Aria expanded belongs on the toggle, not the panel.

Rule 3: body-scroll-lock

While the drawer is open, document.body gets a .menu-open class. The class sets overflow:hidden and disables further scrolling.

This is the difference between a usable mobile drawer and a janky one. Without the lock, scrolling the page (on iOS especially) while the drawer is open causes the page to move under the drawer — disorienting.

body.menu-open {
  overflow: hidden;
  position: fixed;       /* iOS Safari needs position:fixed too */
  width: 100%;
  top: var(--scroll-top); /* preserve visual scroll position */
}

The position:fixed + top reset is necessary on iOS because overflow:hidden alone doesn’t actually prevent scroll on the body. The variable --scroll-top is set just before opening and read just after closing.

Failure shapes:

  • Just overflow:hidden on body. Doesn’t work on iOS Safari.
  • Forget to clear .menu-open when the drawer closes. Page stays locked; user can’t scroll.
  • Forget to reset scroll position after close. Page jumps to top.

The close behaviors

The drawer closes on three user actions:

  1. Escape key — the standard browser/keyboard pattern for dismissing overlays.
  2. Tap on the backdrop#mobileBackdrop is the dimmer layer covering the page.
  3. Tap any link inside the drawer — the drawer closes, the link navigates. Don’t intercept links.

All three are wired in public/scripts/app.js (the Mobile navigation drawer IIFE). Each is one addEventListener call. None of them interfere with the search bar’s / shortcut or the book menu’s aria-expanded (those use different IDs).

What NOT to do

  • Don’t put the drawer inside a <dialog> element with showModal(). ABS uses a regular <nav>.
  • Don’t preventDefault on link clicks inside the drawer. The user clicked a link; let the navigation happen.
  • Don’t use aria-hidden to toggle the drawer. Use the hidden attribute.
  • Don’t add focus-trap code. The drawer is not a modal; focus stays on the button until the user moves it themselves.
  • Don’t disable the hamburger on desktop via JavaScript. Use CSS display:none (already in the Base.astro <style> block).

The / shortcut focuses the search input even when the drawer is open. That’s intentional: the search bar is a parallel surface, not “below” the drawer. To prevent a focus race, the search bar’s IIFE checks document.body.classList.contains('menu-open') and skips the focus shortcut when the drawer is open.

The same applies for the book menu disclosure — its escape handler checks both states.

The integration with the book menu

The book menu (#bookMenuBtn, #bookSubmenu) is a desktop nav for sub-resources of /books/the-agent-advantage/. It’s a separate disclosure widget — never touches the mobile drawer or its backdrop. They live at different ids and different IIFEs.

A user on a tablet (720-1024px width range) sees both: the desktop top bar with the book menu, AND the hamburger button (CSS makes the hamburger visible at ≤720px; the book menu stays visible at all widths).

Verification checklist

#CheckPass
1Drawer opens on hamburger tap at ≤720px viewportYes
2Drawer does not appear at >720px viewportYes (CSS display:none)
3aria-expanded flips correctly on open/closeYes
4Body scroll locks while drawer openYes (iOS tested)
5Escape, backdrop, link click all closeYes
6Search bar / shortcut doesn’t fire when drawer openYes

Sources

Last verified: 2026-07-14 against the live mobile drawer at agenticbotsitter.com (tested at 375px viewport).

Sources

#mobile-nav#drawer#aria#focus-trap#body-scroll-lock

Submit a take

Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.

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