Skip to content
33 changes: 21 additions & 12 deletions src/components/tableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import {useEffect, useRef, useState} from 'react';
import {useEffect, useState} from 'react';

type TreeItem = {
children: TreeItem[];
Expand Down Expand Up @@ -71,24 +71,33 @@ export function TableOfContents({ignoreIds = []}: Props) {
setTreeItems(_tocItems);
}, [ignoreIds]);

// Track current hash to trigger scroll when it changes (e.g., browser back/forward)
const [currentHash, setCurrentHash] = useState('');

useEffect(() => {
// Initialize hash and listen for changes
setCurrentHash(window.location.hash);
const handleHashChange = () => setCurrentHash(window.location.hash);
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);

// Re-scroll to hash anchor after TOC renders to compensate for layout shift.
// The TOC starts empty and populates client-side, which pushes content down
// and causes the browser's initial anchor scroll to land on the wrong section.
const hasScrolledToHash = useRef(false);
// This effect re-runs whenever the hash or treeItems change.
useEffect(() => {
if (hasScrolledToHash.current || treeItems.length === 0) {
return;
}
const hash = window.location.hash;
if (!hash) {
return;
if (treeItems.length === 0 || !currentHash) {
return undefined;
}
hasScrolledToHash.current = true;
requestAnimationFrame(() => {
const id = decodeURIComponent(hash.slice(1));
const rafId = requestAnimationFrame(() => {
const id = decodeURIComponent(currentHash.slice(1));
document.getElementById(id)?.scrollIntoView();
});
}, [treeItems]);
return () => {
cancelAnimationFrame(rafId);
};
Comment thread
sentry[bot] marked this conversation as resolved.
}, [currentHash, treeItems]);
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

return (
<ul>
Expand Down
Loading