Patterns
The Mobile Drawer Scroll-Lock Pattern
By Flora May dela Cruz
A resilient pattern for hamburger drawers that avoids jumpy scroll, stuck pages, and iOS lag when opening mid-scroll.
Purpose
Mobile navigation drawers often fail in the same way: open the hamburger after scrolling, and the page jumps, lags, or feels stuck after close. The issue is usually not the animation. It is scroll locking. This pattern defines a stable lock/unlock contract that preserves position, avoids reflow jitter, and keeps focus behavior accessible.
When to use it
- A mobile off-canvas nav or drawer overlays page content.
- The page can be scrolled before the menu opens.
- You need reliable behavior across iOS Safari and Chromium browsers.
- You want a reusable baseline for any modal-ish overlay, not just nav.
Skip it when: the menu is an inline accordion that does not lock background scroll.
Core framework
1. Lock with fixed body, not overflow-only
overflow: hidden on html/body can look fine at the top of the page, then break when opened mid-scroll on mobile. Use fixed-body lock:
- Capture
scrollYbefore opening. - Add an open-state class on
html. - Set
bodytoposition: fixed. - Offset body with
top: -scrollY. - Set
left/right: 0andwidth: 100%. - Optionally compensate for scrollbar width.
2. Restore on close exactly once
On close:
- Remove the open-state class.
- Clear all temporary body styles.
- Parse the stored offset (or fallback to captured
scrollY). - Call
window.scrollTo(0, restoredY).
This is what prevents the “close menu, jump to weird position” bug.
3. Keep the accessibility contract intact
- Trigger button updates
aria-expanded. - Drawer updates
aria-hidden. - Escape closes.
- Backdrop closes.
- Focus returns to trigger after close.
- Optional: trap focus while open.
4. Close on breakpoint transitions
If viewport crosses into desktop while drawer is open, force-close and unlock. This prevents stale lock state after rotate/resize.
Reusable template
function lockPageScroll() {
if (document.documentElement.classList.contains('overlay-open')) return;
const html = document.documentElement;
const body = document.body;
const y = window.scrollY || window.pageYOffset || 0;
const scrollbarComp = window.innerWidth - html.clientWidth;
html.classList.add('overlay-open');
body.style.position = 'fixed';
body.style.top = `-${y}px`;
body.style.left = '0';
body.style.right = '0';
body.style.width = '100%';
if (scrollbarComp > 0) body.style.paddingRight = `${scrollbarComp}px`;
}
function unlockPageScroll() {
const html = document.documentElement;
const body = document.body;
if (!html.classList.contains('overlay-open')) return;
const top = body.style.top;
html.classList.remove('overlay-open');
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
body.style.width = '';
body.style.paddingRight = '';
const restoredY = top ? Math.abs(parseInt(top, 10)) : 0;
window.scrollTo(0, Number.isFinite(restoredY) ? restoredY : 0);
}
CSS baseline:
html.overlay-open {
overflow: hidden;
}
AI-assisted workflow
Use this prompt to review existing nav code before refactoring:
Audit this mobile drawer implementation for scroll-lock defects.
Find cases where:
- lock relies on overflow-only,
- scroll position is not restored,
- lock state can get stuck on resize,
- aria-expanded / aria-hidden / focus-return are out of sync.
Return:
1) exact bug risks,
2) a fixed-body lock/unlock patch,
3) a small manual test checklist for iOS + Android.
Collaboration considerations
- PM: include “open at mid-scroll, close without jump” as acceptance criteria.
- Engineering: treat lock/unlock as a shared utility, not per-component improvisation.
- QA: test at top, mid-page, and deep scroll on real devices.
- Accessibility: verify focus order and escape behavior while lock is active.
Common failure patterns
- Overflow-only scroll lock on mobile.
- Missing scroll restoration after unlock.
- Forgetting to clear one inline style (usually
toporposition). - No close-on-resize behavior.
- Returning focus to
bodyinstead of the trigger.
Generalized example
A fictional project-management app has a right-side mobile drawer. Users open it while halfway down a task list. With overflow-only lock, close causes a jump and occasional freeze. After switching to fixed-body lock with top: -scrollY and restoring on close, the drawer opens and closes smoothly at every scroll depth, and focus returns to the hamburger button.
Public-safe review (verified before publish)
- No employer or client product names, codenames, or org names
- No customer names, segment sizes, or identifiable details
- No internal metrics, thresholds, OKRs, or telemetry numbers
- No roadmap, ship dates, or future plans
- No architecture, service names, API shapes, or schema fields from real systems
- No screenshots showing real chrome, real data, or recognizable surfaces
- No internal-only workflows, tools, or terminology
- Every example is fictional or abstracted; numbers are illustrative
- A peer outside any employer could read this and learn nothing proprietary
Take this playbook with you
Drop your email to copy the markdown or download the file. One email unlocks every playbook in the Toybox.
No spam. Occasional notes on new playbooks. Unsubscribe in one click.