fix(docs): stop the pinned sidebar running under the site footer (#6422)

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.
This commit is contained in:
Waleed
2026-08-08 12:10:31 -07:00
committed by GitHub
parent 0aae736b07
commit 533afaaedc
3 changed files with 87 additions and 6 deletions
+13 -3
View File
@@ -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);
@@ -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<HTMLDivElement>(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 (
<div ref={sentinelRef} aria-hidden className='pointer-events-none absolute inset-x-0 top-0' />
)
}
+8 -3
View File
@@ -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 (
<footer className='relative z-[22] mt-[120px] w-full border-[var(--border)] border-t bg-[var(--bg)] max-sm:mt-16 max-lg:mt-[88px]'>
<FooterOverlapProbe />
<div className='mx-auto w-full max-w-[1460px] px-20 pt-16 pb-16 max-sm:px-5 max-lg:px-8 max-lg:pt-12 max-lg:pb-12'>
<nav
aria-label='Footer navigation'