From b32127f4920c56ccc9d337b249b57229e721e395 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 8 Aug 2026 12:01:09 -0700 Subject: [PATCH] fix(docs): stop the pinned sidebar running under the site footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar and its divider are fixed to the viewport, so at the end of the page the footer was drawn over them and the lower part of the nav list became unreachable. FooterOverlapProbe publishes how far the footer reaches into the viewport as `--docs-footer-overlap`. The sidebar reads it as `bottom`, so it keeps its full height and slides up out of view as the footer arrives; the divider reads it too but is shortened rather than slid, so it terminates on the footer's top border instead of stopping short. Measured against the viewport rather than the document on purpose: the value is a constant 0 while the footer is off screen, so a content-height change higher up the page cannot move the sidebar. Verified with Playwright at 1280x800 and 2000x1100 — expanding/collapsing an FAQ with the footer off screen moves the sidebar 0px/0px and leaves the content column unchanged, and at the page bottom the sidebar's bottom edge lands within ~1px of the footer's top. --- apps/docs/app/global.css | 16 ++++- .../docs/components/footer/footer-overlap.tsx | 66 +++++++++++++++++++ apps/docs/components/footer/footer.tsx | 11 +++- 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 apps/docs/components/footer/footer-overlap.tsx diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 5f6ea80f42d..e2c10fb937c 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -373,23 +373,33 @@ aside#nd-sidebar [data-radix-scroll-area-viewport] { Safe because the grid columns are explicit (`0px 300px 1fr 268px 0px`), so removing the placeholder from flow leaves its track intact. `left`/`width` are restated because a fixed box no longer derives them from its grid cell, - and `top`/`height` already come from fumadocs' own utility classes. */ + and `height` already comes from fumadocs' own utility classes. + + Anchoring to `bottom` rather than `top` is what keeps the footer off it: the + offset is how far the footer currently reaches into the viewport (published + by `FooterOverlapProbe`), so the sidebar keeps its full height and slides up + out of view as the footer arrives, the way it did before it was pinned. With + no footer on screen the offset is 0 and this resolves back to top: 92px. */ [data-sidebar-placeholder] { position: fixed !important; left: var(--sidebar-offset); width: var(--fd-sidebar-width); + top: auto !important; + bottom: var(--docs-footer-overlap, 0px) !important; } /* Sidebar divider line — pinned for the same reason, and so it stays glued to the sidebar's right edge. Being fixed takes it out of #nd-docs-layout's grid entirely, so it needs no grid placement and cannot skew a content cell; its - position comes from `left`/`top` alone. */ + position comes from `left`/`top`/`bottom` alone. Unlike the sidebar it is + shortened rather than slid, so it runs from the navbar down to the footer's + top border and the two meet instead of the line stopping short. */ #nd-docs-layout::before { content: ""; display: block; position: fixed; top: 92px; /* below navbar */ - height: calc(100dvh - 92px); + bottom: var(--docs-footer-overlap, 0px); left: calc(var(--sidebar-offset) + var(--fd-sidebar-width)); width: 1px; background-color: var(--surface-active); diff --git a/apps/docs/components/footer/footer-overlap.tsx b/apps/docs/components/footer/footer-overlap.tsx new file mode 100644 index 00000000000..68deafe2d88 --- /dev/null +++ b/apps/docs/components/footer/footer-overlap.tsx @@ -0,0 +1,66 @@ +'use client' + +import { useEffect, useRef } from 'react' + +const OVERLAP_PROPERTY = '--docs-footer-overlap' + +/** + * Publishes how many pixels of the viewport bottom the footer currently covers. + * + * The docs sidebar and its divider are pinned to the viewport, so on their own + * they would run underneath the footer at the end of the page. Both read this as + * their `bottom` and stop at the footer's top edge instead — the sidebar slides + * away with the page and the divider meets the footer's border. + * + * It is deliberately measured against the viewport rather than the document, so + * the value only moves while the footer is actually on screen — a content-height + * change higher up the page cannot disturb the sidebar at all. That was the + * regression #6301 fixed and this must not undo. + */ +export function FooterOverlapProbe() { + const sentinelRef = useRef(null) + + useEffect(() => { + const sentinel = sentinelRef.current + if (!sentinel) return + + const root = document.documentElement + let frame = 0 + let published = -1 + + const measure = () => { + frame = 0 + const overlap = Math.max( + 0, + Math.round(window.innerHeight - sentinel.getBoundingClientRect().top) + ) + if (overlap === published) return + published = overlap + root.style.setProperty(OVERLAP_PROPERTY, `${overlap}px`) + } + + const schedule = () => { + if (frame) return + frame = requestAnimationFrame(measure) + } + + measure() + window.addEventListener('scroll', schedule, { passive: true }) + window.addEventListener('resize', schedule) + + const observer = new ResizeObserver(schedule) + observer.observe(document.body) + + return () => { + if (frame) cancelAnimationFrame(frame) + window.removeEventListener('scroll', schedule) + window.removeEventListener('resize', schedule) + observer.disconnect() + root.style.removeProperty(OVERLAP_PROPERTY) + } + }, []) + + return ( +
+ ) +} diff --git a/apps/docs/components/footer/footer.tsx b/apps/docs/components/footer/footer.tsx index 75896de0aed..4009cc1de72 100644 --- a/apps/docs/components/footer/footer.tsx +++ b/apps/docs/components/footer/footer.tsx @@ -1,4 +1,5 @@ import Link from 'next/link' +import { FooterOverlapProbe } from '@/components/footer/footer-overlap' import { SimWordmark } from '@/components/ui/sim-logo' import { SIM_SITE_URL } from '@/lib/urls' @@ -133,13 +134,17 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) /** * Site footer. * - * `relative z-[22]` stacks it above the docs sidebar (z-20) and that sidebar's - * divider (z-21), both of which are pinned to the viewport, so the footer slides - * over them at the end of the page instead of being drawn through. + * The docs sidebar and its divider are pinned to the viewport, so they would run + * underneath a full-bleed footer at the end of the page. `FooterOverlapProbe` + * publishes how far the footer reaches into the viewport and both stop there + * instead. `relative z-[22]` stacks the footer above the sidebar (z-20) and its + * divider (z-21) so that, before the probe's first measurement, the footer covers + * them rather than being drawn through. */ export function Footer() { return (