Skip to content

fix(website): portal mobile nav sheet to body so it covers the viewport#670

Open
ambicuity wants to merge 1 commit into
rohitg00:mainfrom
ambicuity:fix/mobile-nav-sheet-portal
Open

fix(website): portal mobile nav sheet to body so it covers the viewport#670
ambicuity wants to merge 1 commit into
rohitg00:mainfrom
ambicuity:fix/mobile-nav-sheet-portal

Conversation

@ambicuity
Copy link
Copy Markdown

@ambicuity ambicuity commented May 27, 2026

Closes #669.

Problem

On mobile (viewport ≤ 720px), tapping the hamburger opens the navigation sheet, but the dark overlay only covers the top header strip — the hero ("AGENT MEMORY", "THE MEMORY LAYER YOUR CODING AGENT…", etc.) shows through, and the menu items themselves are barely visible because the panel is squeezed into the header strip.

Root cause

Nav.module.css applies backdrop-filter: blur(10px) to the <header>. Per the CSS spec, backdrop-filter (with any non-none value) establishes a containing block for fixed-positioned descendants — same behavior as transform or filter.

The mobile sheet was rendered as a child of that header:

// website/components/Nav.tsx
<header className={styles.nav}>
  ...
  <MobileNavToggle sections={SECTIONS} stars={stats.stars} />
</header>

So position: fixed; inset: 0 on .sheet resolved against the header's bounding box — the thin top strip — not the viewport.

Fix

Render the sheet through createPortal(sheet, document.body) so it escapes the nav's containing block and is positioned against the viewport. Also:

  • Use var(--abyss) for the background so the overlay is fully opaque (no faint hero bleed-through on browsers that compose rgba(0,0,0,0.94) against bright content).
  • Drop backdrop-filter on the sheet itself — it's redundant once the bg is opaque and was the same property responsible for the original containing-block bug at the nav level.
  • Keep z-index: 95 so the nav header (z-index: 100) — and therefore the brand + close (×) button — stays on top of the open sheet.
  • Render-on-mount guard (mounted state) to avoid SSR/hydration mismatch when calling createPortal(document.body).

Verification

Built and run locally (npm run build && npm start), opened at 390 × 844 (iPhone 14 Pro) viewport:

Before (from issue): hero bleeds through, menu items invisible.

After:

  • Mobile menu open: opaque background, all items (STACK / FEATURES / CONTROL / DEMO / VS / INSTALL) clearly visible, footer (GITHUB / NPM / CHANGELOG) visible, nav header with × close button still on top and tappable.
  • Desktop (1280 × 800): no regression, nav layout unchanged.
  • npm run build passes (Next 16.2.6 Turbopack, TypeScript clean).

Files changed

  • website/components/MobileNavToggle.tsx — portal sheet to document.body behind a mount guard; no behavior changes to the button, focus handling, or scroll lock.
  • website/components/MobileNavToggle.module.css — opaque bg via var(--abyss), drop backdrop-filter on .sheet, retain z-index: 95.

Test plan

  • Mobile (≤ 720px) hamburger open → opaque, all items visible, no content bleed
  • Mobile close (× button + ESC + backdrop click) still works
  • Desktop nav unaffected
  • npm run build green
  • No body scroll while menu open

Summary by CodeRabbit

  • Bug Fixes

    • Improved mobile navigation sheet rendering and initialization to ensure stable, consistent display during page load.
  • Style

    • Updated mobile navigation sheet background styling for enhanced visual appearance and improved clarity.

Review Change Stack

The mobile nav sheet was rendered inside the <header> element, which uses
backdrop-filter: blur(10px). Per CSS spec, backdrop-filter establishes a
containing block for fixed-positioned descendants, so the sheet's
`position: fixed; inset: 0` was clipped to the header strip instead of
covering the viewport. The dark overlay only painted across the top bar,
leaving the hero fully visible behind the menu and the panel items
squeezed into a strip too short to render.

Render the sheet through createPortal to document.body so it escapes the
nav's containing block. Drop backdrop-filter on the sheet (no longer
needed once the bg is fully opaque) and switch to var(--abyss) so hero
content can't bleed through even if a browser ignores 0.94 opacity.

Closes rohitg00#669
Copilot AI review requested due to automatic review settings May 27, 2026 03:45
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

@ambicuity is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 634b0c4d-9b6f-44e6-80bd-e367752f1571

📥 Commits

Reviewing files that changed from the base of the PR and between 6939d4a and 099ebc3.

📒 Files selected for processing (2)
  • website/components/MobileNavToggle.module.css
  • website/components/MobileNavToggle.tsx

📝 Walkthrough

Walkthrough

MobileNavToggle now renders the mobile navigation sheet via a React portal to document.body instead of inline within the header. This escapes the backdrop-filter containing block that was limiting fixed positioning. Mount gating prevents DOM access before hydration, and the sheet background is changed to a solid opaque color.

Changes

Mobile Nav Sheet Portal Migration

Layer / File(s) Summary
Portal infrastructure and mount gating
website/components/MobileNavToggle.tsx
createPortal is imported from react-dom and mounted state with useEffect provides client-side gating to prevent DOM portal access before component mount.
Sheet content refactoring and portal rendering
website/components/MobileNavToggle.tsx
Sheet markup is moved into a constant with documentation explaining the portal-to-body pattern and why it is necessary for fixed positioning to work correctly with backdrop-filter. The return statement conditionally renders the sheet via createPortal(sheet, document.body) only after mount.
Sheet background color update
website/components/MobileNavToggle.module.css
The .sheet background is changed from semi-transparent rgba with backdrop-filter blur to solid var(--abyss), ensuring the overlay is fully opaque and prevents underlying hero content from bleeding through.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A sheet that floated in the header's blur,
Now portals free to viewport, crisp and pure,
Fixed positioning fixed, the hero's veiled at last,
With solid shadows—the mobile nav, steadfast!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: portaling the mobile nav sheet to body to fix viewport coverage.
Linked Issues check ✅ Passed All coding requirements from issue #669 are met: sheet is portaled to document.body, mount guard prevents SSR/hydration mismatch, opaque background replaces semi-transparent, and z-index positioning ensures proper layering.
Out of Scope Changes check ✅ Passed All changes directly address the viewport coverage bug; CSS styling and portal implementation are focused and necessary for the fix without extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the mobile navigation sheet to render via a React portal so it’s not clipped by the header’s backdrop-filter containing block.

Changes:

  • Portal the mobile nav sheet to document.body using createPortal and a client-only mounted guard
  • Adjust the sheet background styling and add documentation comments about stacking context

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
website/components/MobileNavToggle.tsx Portals the mobile sheet to <body> to avoid clipping and adds a mount guard for client-only DOM access.
website/components/MobileNavToggle.module.css Updates sheet background and documents z-index/stacking context expectations for the portaled overlay.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +47
className={`${styles.sheet} ${open ? styles.sheetOpen : ""}`}
aria-hidden={!open}
Comment on lines +52 to +58
<nav className={styles.panel} aria-label="Site navigation">
<ul className={styles.list}>
{sections.map((s) => (
<li key={s.href}>
<a href={s.href} onClick={() => setOpen(false)}>
{s.label}
</a>
</div>
</nav>
</div>
{mounted ? createPortal(sheet, document.body) : null}
Comment on lines +63 to +67
<a
href="https://github.com/rohitg00/agentmemory"
target="_blank"
rel="noopener"
onClick={() => setOpen(false)}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mobile hamburger menu overlay is transparent — hero content shows through

2 participants