Skip to content

Commit 533afaa

Browse files
authored
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.
1 parent 0aae736 commit 533afaa

3 files changed

Lines changed: 87 additions & 6 deletions

File tree

apps/docs/app/global.css

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,23 +373,33 @@ aside#nd-sidebar [data-radix-scroll-area-viewport] {
373373
Safe because the grid columns are explicit (`0px 300px 1fr 268px 0px`), so
374374
removing the placeholder from flow leaves its track intact. `left`/`width`
375375
are restated because a fixed box no longer derives them from its grid cell,
376-
and `top`/`height` already come from fumadocs' own utility classes. */
376+
and `height` already comes from fumadocs' own utility classes.
377+
378+
Anchoring to `bottom` rather than `top` is what keeps the footer off it: the
379+
offset is how far the footer currently reaches into the viewport (published
380+
by `FooterOverlapProbe`), so the sidebar keeps its full height and slides up
381+
out of view as the footer arrives, the way it did before it was pinned. With
382+
no footer on screen the offset is 0 and this resolves back to top: 92px. */
377383
[data-sidebar-placeholder] {
378384
position: fixed !important;
379385
left: var(--sidebar-offset);
380386
width: var(--fd-sidebar-width);
387+
top: auto !important;
388+
bottom: var(--docs-footer-overlap, 0px) !important;
381389
}
382390

383391
/* Sidebar divider line — pinned for the same reason, and so it stays glued to
384392
the sidebar's right edge. Being fixed takes it out of #nd-docs-layout's grid
385393
entirely, so it needs no grid placement and cannot skew a content cell; its
386-
position comes from `left`/`top` alone. */
394+
position comes from `left`/`top`/`bottom` alone. Unlike the sidebar it is
395+
shortened rather than slid, so it runs from the navbar down to the footer's
396+
top border and the two meet instead of the line stopping short. */
387397
#nd-docs-layout::before {
388398
content: "";
389399
display: block;
390400
position: fixed;
391401
top: 92px; /* below navbar */
392-
height: calc(100dvh - 92px);
402+
bottom: var(--docs-footer-overlap, 0px);
393403
left: calc(var(--sidebar-offset) + var(--fd-sidebar-width));
394404
width: 1px;
395405
background-color: var(--surface-active);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client'
2+
3+
import { useEffect, useRef } from 'react'
4+
5+
const OVERLAP_PROPERTY = '--docs-footer-overlap'
6+
7+
/**
8+
* Publishes how many pixels of the viewport bottom the footer currently covers.
9+
*
10+
* The docs sidebar and its divider are pinned to the viewport, so on their own
11+
* they would run underneath the footer at the end of the page. Both read this as
12+
* their `bottom` and stop at the footer's top edge instead — the sidebar slides
13+
* away with the page and the divider meets the footer's border.
14+
*
15+
* It is deliberately measured against the viewport rather than the document, so
16+
* the value only moves while the footer is actually on screen — a content-height
17+
* change higher up the page cannot disturb the sidebar at all. That was the
18+
* regression #6301 fixed and this must not undo.
19+
*/
20+
export function FooterOverlapProbe() {
21+
const sentinelRef = useRef<HTMLDivElement>(null)
22+
23+
useEffect(() => {
24+
const sentinel = sentinelRef.current
25+
if (!sentinel) return
26+
27+
const root = document.documentElement
28+
let frame = 0
29+
let published = -1
30+
31+
const measure = () => {
32+
frame = 0
33+
const overlap = Math.max(
34+
0,
35+
Math.round(window.innerHeight - sentinel.getBoundingClientRect().top)
36+
)
37+
if (overlap === published) return
38+
published = overlap
39+
root.style.setProperty(OVERLAP_PROPERTY, `${overlap}px`)
40+
}
41+
42+
const schedule = () => {
43+
if (frame) return
44+
frame = requestAnimationFrame(measure)
45+
}
46+
47+
measure()
48+
window.addEventListener('scroll', schedule, { passive: true })
49+
window.addEventListener('resize', schedule)
50+
51+
const observer = new ResizeObserver(schedule)
52+
observer.observe(document.body)
53+
54+
return () => {
55+
if (frame) cancelAnimationFrame(frame)
56+
window.removeEventListener('scroll', schedule)
57+
window.removeEventListener('resize', schedule)
58+
observer.disconnect()
59+
root.style.removeProperty(OVERLAP_PROPERTY)
60+
}
61+
}, [])
62+
63+
return (
64+
<div ref={sentinelRef} aria-hidden className='pointer-events-none absolute inset-x-0 top-0' />
65+
)
66+
}

apps/docs/components/footer/footer.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Link from 'next/link'
2+
import { FooterOverlapProbe } from '@/components/footer/footer-overlap'
23
import { SimWordmark } from '@/components/ui/sim-logo'
34
import { SIM_SITE_URL } from '@/lib/urls'
45

@@ -133,13 +134,17 @@ function FooterColumn({ title, items }: { title: string; items: FooterItem[] })
133134
/**
134135
* Site footer.
135136
*
136-
* `relative z-[22]` stacks it above the docs sidebar (z-20) and that sidebar's
137-
* divider (z-21), both of which are pinned to the viewport, so the footer slides
138-
* over them at the end of the page instead of being drawn through.
137+
* The docs sidebar and its divider are pinned to the viewport, so they would run
138+
* underneath a full-bleed footer at the end of the page. `FooterOverlapProbe`
139+
* publishes how far the footer reaches into the viewport and both stop there
140+
* instead. `relative z-[22]` stacks the footer above the sidebar (z-20) and its
141+
* divider (z-21) so that, before the probe's first measurement, the footer covers
142+
* them rather than being drawn through.
139143
*/
140144
export function Footer() {
141145
return (
142146
<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]'>
147+
<FooterOverlapProbe />
143148
<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'>
144149
<nav
145150
aria-label='Footer navigation'

0 commit comments

Comments
 (0)