Skip to content

fix(feed): stop sticky tab bar from clipping the top of the feed#455

Merged
spe1020 merged 1 commit into
mainfrom
fix/feed-header-sticky-tabs-offset
Jun 19, 2026
Merged

fix(feed): stop sticky tab bar from clipping the top of the feed#455
spe1020 merged 1 commit into
mainfrom
fix/feed-header-sticky-tabs-offset

Conversation

@spe1020

@spe1020 spe1020 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

The Global/Following/Replies/Groups tab bar on the feed (and the kitchen tab bar) pinned at twice the header height, leaving a header-tall gap where feed content scrolled through and got clipped — on both desktop and mobile (reproduced in plain browsers, not PWA/Capacitor specific).

Root cause

#app-scroll reserves space for the fixed header via padding-top: var(--header-h). A position: sticky child's top offset is measured from that container's content box — i.e. after the padding. So top: var(--header-h) on the tab bar stacked a second header-height on top, pinning it at 2 × --header-h.

The fix: the padding already accounts for the header, so the sticky offset must be top: 0.

Also: --header-h is now a CSS-deterministic single source of truth

Previously --header-h came from a JS bind:clientHeight measurement seeded at 64px, which lagged the real height (74px desktop / ~62px mobile) for ~3s on load (ResizeObserver race), and a hardcoded mobile override (top: calc(56px + env(...))) baked in a per-viewport magic number.

  • --header-row-h / --header-h are defined in app.css, responsive at the sm (640px) breakpoint — where the search bar toggles the header content height (measured: 37.77px → 50.36px). This is the correct breakpoint; 1023px would clip the search bar between 640–1023px.
  • The safe-area inset stays dynamic via max(env(safe-area-inset-top), 0.75rem) — no JS, works at all widths (env is 0 on desktop/non-notched).
  • The header content row (.zh-root) is pinned to --header-row-h, so the painted header height equals --header-h on the first frame, independent of font/image load timing.
  • Removed the JS bind:clientHeight measurement and the hardcoded mobile tab-bar top override.

Before / after (pinned tab bar, measured via Playwright)

Viewport header bottom tab top before tab top after
Desktop 1440 75px 150px (2×) → 75px gap 75px → 0 gap
Mobile 390 62px 124px (2×) → 62px gap 62px → 0 gap

Gap is 0 at first paint and steady state on both breakpoints.

Files

  • src/app.css — define --header-row-h / --header-h (responsive at 640px)
  • src/components/Header.svelte — pin .zh-root to --header-row-h
  • src/routes/+layout.svelte — drop JS header measurement; #app-scroll uses var(--header-h); .header-blur safe-area padding at all widths
  • src/routes/community/+page.svelte — tab bar top: 0; remove mobile override
  • src/routes/kitchen/+page.svelte — tab bar top: 0 (same double-count bug)

Test plan

  • Desktop web: feed loads with tab bar flush under the header, no clipped first post; no reflow jump on load
  • Mobile web (iOS Safari): same, with safe-area inset respected on notched devices
  • Kitchen tab bar pins correctly below the header
  • Scroll up/down: tab bar hide/show animation still works

Made with Cursor

The sticky relay tab bar (community + kitchen) pinned at twice the header
height, leaving a header-tall gap where feed content scrolled through and
was clipped, on both desktop and mobile.

Root cause: #app-scroll reserves the fixed header via
padding-top: var(--header-h), and a sticky child's `top` offset is measured
from that container's content box (after the padding). Setting
top: var(--header-h) on the tabs stacked a second header-height on top,
pinning them at 2x. Fix: the padding already accounts for the header, so the
sticky offset is top: 0.

Also make --header-h a CSS-deterministic single source of truth so the
reserved space is correct on the first frame with no ResizeObserver race:
- Define --header-row-h / --header-h in app.css, responsive at the sm (640px)
  breakpoint where the search bar toggles the content height. The safe-area
  inset stays dynamic via max(env(safe-area-inset-top), 0.75rem).
- Pin the header content row (.zh-root) to --header-row-h so the painted
  header height never depends on font/image load timing.
- Apply .header-blur's safe-area top padding at all widths so the painted
  header always equals --header-h.
- Drop the JS bind:clientHeight measurement and the hardcoded mobile tab-bar
  top override (top: calc(56px + env(...))).

Verified: header bottom == pinned tab-bar top == reserved space, gap 0 at
first paint and steady state, on desktop (1440) and mobile (390).

Co-authored-by: Cursor <[email protected]>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying frontend with  Cloudflare Pages  Cloudflare Pages

Latest commit: e69199a
Status: ✅  Deploy successful!
Preview URL: https://e21aea93.frontend-hvd.pages.dev
Branch Preview URL: https://fix-feed-header-sticky-tabs.frontend-hvd.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
zapcooking-frontend e69199a Jun 19 2026, 01:47 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
frontend e69199a Jun 19 2026, 01:49 PM

@spe1020 spe1020 merged commit 557990e into main Jun 19, 2026
5 of 7 checks passed
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