Skip to main content

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.

navigationmobileaccessibilityinteraction-design

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:

  1. Capture scrollY before opening.
  2. Add an open-state class on html.
  3. Set body to position: fixed.
  4. Offset body with top: -scrollY.
  5. Set left/right: 0 and width: 100%.
  6. Optionally compensate for scrollbar width.

2. Restore on close exactly once

On close:

  1. Remove the open-state class.
  2. Clear all temporary body styles.
  3. Parse the stored offset (or fallback to captured scrollY).
  4. 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

  1. Overflow-only scroll lock on mobile.
  2. Missing scroll restoration after unlock.
  3. Forgetting to clear one inline style (usually top or position).
  4. No close-on-resize behavior.
  5. Returning focus to body instead 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.