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.

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>withshowModal(). 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
hiddenattribute. Screen readers still announce the drawer as available. - Use the inverse:
aria-hiddenon the drawer. That’s the wrong aria attribute;hiddenis correct here. - Set
aria-expandedon 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:hiddenon body. Doesn’t work on iOS Safari. - Forget to clear
.menu-openwhen 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:
Escapekey — the standard browser/keyboard pattern for dismissing overlays.- Tap on the backdrop —
#mobileBackdropis the dimmer layer covering the page. - 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 withshowModal(). ABS uses a regular<nav>. - Don’t
preventDefaulton link clicks inside the drawer. The user clicked a link; let the navigation happen. - Don’t use
aria-hiddento toggle the drawer. Use thehiddenattribute. - 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 theBase.astro<style>block).
The integration with the search bar
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
| # | Check | Pass |
|---|---|---|
| 1 | Drawer opens on hamburger tap at ≤720px viewport | Yes |
| 2 | Drawer does not appear at >720px viewport | Yes (CSS display:none) |
| 3 | aria-expanded flips correctly on open/close | Yes |
| 4 | Body scroll locks while drawer open | Yes (iOS tested) |
| 5 | Escape, backdrop, link click all close | Yes |
| 6 | Search bar / shortcut doesn’t fire when drawer open | Yes |
Sources
- MDN: aria-expanded
- W3C APG: Disclosure Pattern
- ABS companion: ABS Search Bar: The Cheap State Update, Not a Popup
- ABS companion: Three-SHA Check Before Pushing
Last verified: 2026-07-14 against the live mobile drawer at agenticbotsitter.com (tested at 375px viewport).


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.