fix: navbar — priority+ overflow, alignment, click-through guard#103
Merged
Conversation
Adding Blog + About pushed the 7-link in-nav past its horizontal
budget on narrow desktop widths (~800–1100px) and labels wrapped
into a multi-line stack. The mobile hamburger only kicks in below
md (<768px) so the middle zone was uncovered. This change fixes
the overflow plus four follow-on issues uncovered while iterating.
Priority+ dropdown ("More" overflow).
* Trailing links collapse into a "More" dropdown instead of
wrapping. ResizeObserver re-measures on container resize.
* Each item's natural width is captured ONCE on mount while
every item is still visible and then reused — re-measuring
items that are display:none reads scrollWidth === 0, which
under-counts and causes the algorithm to oscillate.
* useLayoutEffect (not useEffect) so the collapse lands in the
same paint as the initial render. No flash of all-7-inline.
* Overflowed items stay in the DOM as display:none +
aria-hidden + tabIndex -1, so a future width change can
re-show them without remounting and screen-reader users
don't see them twice.
Layout.
* `justify-start` (the flex default) keeps overflowing items
bleeding rightward only, never left into the logo.
* `mx-8` (NOT `px-8`) gives a visible gap between the logo and
the nav, and between the nav and the right-actions cluster.
Padding leaves clientWidth = container width including the
padding, which over-budgets the overflow algorithm by 64px
and lets items visually touch the Ask Support button.
Margin shrinks the flex-1 allocation itself.
* NO `overflow-hidden` on the container. The dropdown panel is
absolutely positioned below the container, and overflow-hidden
would clip it (the user clicks More and sees an empty popup).
The cached-widths algorithm makes the collapse reliable
without needing visual clipping as a fallback.
GitHub link removed from the in-nav. It's already in the hero CTAs
and the footer Community column.
"More" alignment.
* `<button>` ships with intrinsic padding + border (~8px) even
after Tailwind v4's preflight, plus a different baseline than
`<a>` in flex children. `p-0 m-0 border-0 bg-transparent
appearance-none cursor-pointer` strips the chrome, but the
baseline still ends up ~3-5px lower than the sibling <a>'s.
* The fix that empirically lands: wrap the SVG in a span with
`vertical-align: -0.75em`. That expands the button's line-box
downward, the parent flex's items-center pulls the bbox back
up to match the <a>'s, and the text baseline aligns exactly.
* The chevron then needs `top: -0.7em` to come back up to text
x-height middle (otherwise it hangs off the bottom of the
line). The pair (-0.75em wrapper / -0.7em SVG) is the working
set found empirically.
Click-through guard.
* HeroBackground listens on `window` for mousemove → particle
hover tooltip. A passive z-40 backdrop didn't help because
the events still bubble through to window.
* Both the backdrop AND the panel (they're DOM siblings, so a
panel event never bubbles through the backdrop) attach
onMouseMove / onMouseOver / onPointerMove handlers that call
`stopBubble` — a tiny module-level helper that
`e.stopPropagation()`s. The window listener now never fires
while the menu is open, so the particle tooltip stays off
behind the dropdown.
Accessibility.
* The More trigger is a real <button> with aria-haspopup="menu",
aria-expanded, aria-label.
* The panel is role="menu" with role="menuitem" children.
* Escape / outside click / route change all close the menu.
Anti-future-drift. Adding new nav items doesn't break the header
again — they just join the More list at narrower widths.
Verified at 700 / 850 / 950 / 1100 / 1280px via headless Chrome
screenshots and user inspection. All four gates (build / vitest /
lint / typecheck) green.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
6 tasks
smaramwbc
added a commit
that referenced
this pull request
May 25, 2026
React error #418 surfaced in production after PR #103 shipped: the HomePage's `showHeroCanvas` useState initialiser branched on window.matchMedia at first render. On SSR it returned false (typeof window === 'undefined' guard); on the client first render (during hydration) it returned !matches — true on desktop. The conditional {showHeroCanvas && <HeroBackground />} then differed between the prerendered HTML and the hydrating client → React detected a tree mismatch at the subtree, blew up the whole hydration, and re-rendered the entire root from scratch on the client. Two-pass pattern fix: initial state is `false` on both SSR and the first client render, so hydration sees the same tree shape on both sides. After hydration the effect reads matchMedia and flips the flag — a normal post-mount re-render mounts HeroBackground on desktop. Brief flash of "no background" is invisible because the canvas is decorative and the hero content paints from the prerendered HTML regardless. Same pattern as the ThemeProvider fix in the SSR PR — anything that reads window / matchMedia / localStorage in a useState initialiser during render is an SSR/client mismatch waiting to happen. The useState default must match what SSR produces; the real value arrives in a mount effect. Why this only surfaced now: the navbar PR #103 happened to expose it because the navbar's ResizeObserver + useLayoutEffect path triggered extra hydration scrutiny in React 19's dev / error-recovery flow. The bug was latent since PR #94 (lazy HeroBackground); React was silently recovering and re-rendering the root before, but it counts as an error now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adding Blog + About pushed the 7-link in-nav past its horizontal budget on narrow desktop widths (≈800–1100 px), and labels wrapped into a multi-line stack. The mobile hamburger only kicks in below `md` (<768 px) so the middle zone was uncovered. This change fixes the overflow plus four follow-on issues we uncovered while iterating.
What it does
Plus housekeeping: GitHub link removed from the in-nav (it's already in the hero CTAs and the footer Community column), `whitespace-nowrap` on every link label, `shrink-0` on the right-actions cluster.
Accessibility
Anti-future-drift
Adding new nav items doesn't break the header again — they just join the More list at narrower widths.
Verification
Rollback
Single-file change. `git revert` restores the previous nav (broken at narrow widths).