Skip to content

Keep the mobile header and announcements pinned while the page scrolls - #1181

Open
busbyk wants to merge 5 commits into
mainfrom
fix/mobile-header-sticky
Open

Keep the mobile header and announcements pinned while the page scrolls#1181
busbyk wants to merge 5 commits into
mainfrom
fix/mobile-header-sticky

Conversation

@busbyk

@busbyk busbyk commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Description

Below lg, the header nav did not stay pinned while scrolling — it drifted over the page content instead. Found while reviewing the native danger map, but it predates that work and affects every page, so it is split out here rather than shipped inside a large feature PR.

Fixing the header surfaced several adjacent problems, and reviewing it turned up two more mobile header bugs that were already there. Those are all in here as separate commits, since they are separable and read better on their own.

Related Issues

None. Pre-existing; noticed incidentally.

Key Changes

The header pins below lg. MobileNav was position: fixed with no top. A fixed element without an offset resolves to its static position — beneath the announcement banners — and then holds that viewport offset forever, so everything scrolled underneath it and the nav sat over the content. The h-[64px] spacer that reserved the space the fixed nav took out of the flow is gone with it, since a sticky element occupies its own space.

The sticky element is a wrapper around the banners and the header, not the header itself. A sticky element only stays put while its containing block is in view, and neither the nav nor the header is much taller than its own box, so either one alone would unstick almost immediately. Announcements.tsx now wraps the banners and the header in one sticky block. Above lg that wrapper is static and everything behaves exactly as it does on main.

Announcements pin with the header. Previously the banners sat above the header in the flow and scrolled away. Expanding one while scrolled down did nothing visible: the banner grew off the top of the screen and scroll anchoring held your place, so the only way to read it was to scroll back to the top. Now it is on screen wherever you are. Because a pinned banner is permanent screen real estate, the banners cap at 35dvh below lg and scroll internally past that — a 2000px announcement renders 282px tall on an 806px viewport instead of taking the whole phone.

The header no longer covers the admin bar. AdminBar is fixed top-0 z-50; the header is also z-50 and later in the DOM, so a header pinned at top: 0 won and hid it from logged-in editors. AdminBar now publishes its height as --admin-bar-height and the sticky wrapper pins below it. Raising the admin bar's z-index instead would have put it over the hamburger button.

The mobile menu panel tracks the header. Its top and max-height now both derive from the same measured header bottom (in dvh), where the max-height was previously hardcoded against a 64px offset and could run the panel past the bottom of the viewport with no way to scroll to it. The measurement re-runs on a frame while the menu is open, so the panel follows the header when the banners animate open or closed underneath it.

The logo is centered in the mobile header. The row was flex justify-between over three children, so when a center had no active announcements the toggle did not render and the logo was pushed to the right edge. A spacer opposite the hamburger would not have fixed it either — the hamburger is 40px and the toggle is 36px, so the logo was a couple of pixels off center even when the toggle was present. It is three grid tracks with equal outer columns now.

Anchor targets clear the pinned header via scroll-pt-16 lg:scroll-pt-0. There are no in-page anchors in the app today; this is so the next one does not land underneath the header.

Expanding from the desktop pill scrolls back to the banners. Desktop has the same off-screen-expansion problem, and since the header is not sticky there, pinning is not the answer — the fixed pill returns you to the top instead, honoring prefers-reduced-motion.

How to test

  1. Open any tenant page (e.g. nwac.localhost:3000) at a viewport narrower than 1024px.
  2. Scroll down. The header should pin to the top of the viewport with content passing beneath it. Before this change it drifted over the content and overlapped it.
  3. With an announcement active, collapse it from the megaphone toggle, scroll down, then expand it again. The announcement should appear pinned above the header without moving you off your place on the page. Collapsing should likewise leave you where you are.
  4. Open the mobile menu after scrolling. The dropdown should align flush to the bottom of the header, and should not extend past the bottom of the viewport. Collapse the announcements from the megaphone while the menu is open — the dropdown should follow the header up.
  5. On a center with no active announcements (SAC in the local seed), check that the logo is centered rather than pushed right.
  6. Log in and repeat at mobile widths: the admin bar should stay visible above the pinned header rather than being covered by it.
  7. At lg and wider, everything should be unchanged — static header, banners scrolling away, and the collapsed pill scrolling you back to the top when expanded.

Screenshots / Demo video

Demo of the original sticky-header commit: https://www.loom.com/share/49c80858169d46579c391bf6b582ec06

Note that the Loom predates the later commits — it shows the banners scrolling away before the header pins, which is no longer how it behaves. Worth re-recording before merge.

Migration Explanation

None — presentational only.

Future enhancements / Questions

  • The 35dvh cap on pinned announcements is a judgment call, not a researched number. Worth a look on a real device with a genuinely long announcement.
  • Verified in Chrome at emulated mobile and desktop widths against the local seed, not on physical hardware. iOS Safari is the one worth a spot check, since it is the primary mobile target and dvh plus sticky is where it historically differs.
  • The desktop header is still not sticky. That is deliberate and unchanged here, but if it should be, the wrapper is now the natural place to do it.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Below `lg` the header nav was `position: fixed` with no `top`, so it settled
at its static position — beneath the announcement banners — and stayed at that
viewport offset. Everything above and below then scrolled underneath it, and
the nav ended up sitting over the page content rather than above it.

`top-0` alone would not fix this. A sticky element only stays put while its
containing block is in view, and the nav's containing block is `<header>`,
which is barely taller than the nav itself — it would unstick immediately. So
the header is now the sticky element and the nav is plain in-flow content.

That also removes the `h-[64px]` spacer, which existed only to reserve the
space the fixed nav took out of the flow. A sticky header occupies its own
space, so the spacer would now double it.

Announcement banners sit above the header and scroll away before it pins,
which is the intended behaviour. Desktop is unchanged (`lg:static`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Preview deployment: https://fixxmobile-header-sticky.preview.avy-fx.org

busbyk and others added 2 commits August 11, 2026 14:06
Pinning the header surfaced four problems, three of them mine.

The admin bar is fixed to the top of the viewport, so a header pinned at
top: 0 covered it once the page scrolled. Both are z-50 and the header
comes later in the DOM, so it won. Raising the admin bar instead would
have put it over the hamburger button, trading one broken control for
another, so the admin bar now publishes its height as --admin-bar-height
and the header pins below it.

The menu panel's max height was hardcoded against a 64px offset while its
top came from a measurement, so with announcement banners expanded it
could run past the bottom of the viewport. The page doesn't scroll while
the menu is open, which left the tail unreachable. Both now come from the
same measured value, in dvh so mobile browser chrome doesn't reintroduce
it.

That measurement only ran on open and on resize. Collapsing the banners
from the megaphone while the menu was open moved the header and left the
panel behind. It now tracks on a frame for as long as the menu is open,
which is the only time the value is used.

Anchor targets had nothing keeping them clear of the pinned header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row was flex justify-between over three children, so when a center
had no active announcements the toggle didn't render and the logo got
pushed to the right edge.

A spacer opposite the hamburger wouldn't have centered it either: the
hamburger is 40px and the toggle is 36px, so the logo sat a couple of
pixels off center even when the toggle was there. Three grid tracks with
equal outer columns center it in both cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The banners sit above the header in the page flow, but both controls that
expand them stay put while the page scrolls: the toggle in the sticky
mobile header and the fixed pill on desktop. Expanding from either one
grew the banner off the top of the screen, and scroll anchoring then held
the reader's place, so tapping the toggle appeared to do nothing at all.
The only way to read the announcement was to scroll back to the top and
find it there.

Expanding now returns to the top of the page, where the banners are, and
honors prefers-reduced-motion. Collapsing still leaves you where you are,
and the banners auto-expanding on first view goes through the provider
directly, so a fresh page load doesn't scroll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expanding an announcement while scrolled took you to the top of the page
to read it, because the banners lived above the header in the flow and
scrolled away with it. Pinning them alongside the header is the better
answer: the announcement is visible wherever you are, and you keep your
place on the page.

The sticky element is now a wrapper around the banners and the header
rather than the header itself, so the two pin as one block. That makes
the scroll to the top redundant on mobile, so the toggle there no longer
does it. Desktop is unchanged — the header isn't sticky there and the
collapsed pill still scrolls you back up, so it keeps that behavior.

Pinning is what makes a long announcement dangerous, since it would sit
on the screen permanently, so below lg the banners cap at 35dvh and
scroll past that. A 2000px announcement renders 282px tall on a 806px
viewport, leaving the header and the page reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@busbyk busbyk changed the title Keep the mobile header pinned while the page scrolls Keep the mobile header and announcements pinned while the page scrolls Aug 11, 2026
@busbyk
busbyk marked this pull request as ready for review August 11, 2026 22:08
@busbyk
busbyk requested a review from rchlfryn August 11, 2026 22:10
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.

1 participant