From 6217a2ed3b82a3046eb2ac3495235e895cd4ff31 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 20 Aug 2025 15:55:16 +0200 Subject: [PATCH] Page outline: scroll to active item (#3578) --- .changeset/three-queens-pretend.md | 5 ++++ .../src/components/PageAside/PageAside.tsx | 13 ++++----- .../PageAside/ScrollSectionsList.tsx | 28 ++++++++++++++++--- 3 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 .changeset/three-queens-pretend.md diff --git a/.changeset/three-queens-pretend.md b/.changeset/three-queens-pretend.md new file mode 100644 index 000000000..138fb4bc8 --- /dev/null +++ b/.changeset/three-queens-pretend.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Page outline: scroll to active item diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index 4f3604fc8..c9f74ff41 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -40,6 +40,7 @@ export function PageAside(props: { 'group/aside', 'hidden', 'pt-8', + 'pb-4', 'xl:flex', 'xl:max-3xl:chat-open:hidden', @@ -111,7 +112,7 @@ export function PageAside(props: { > {page.layout.outline ? ( -
+
{document ? ( @@ -165,11 +166,7 @@ async function PageAsideSections(props: { document: JSONDocument; context: GitBo const sections = await getDocumentSections(context, document); - return sections.length > 1 ? ( -
- -
- ) : null; + return sections.length > 1 ? : null; } function PageAsideActions(props: { @@ -199,7 +196,7 @@ function PageAsideActions(props: { 'border-t', 'first:border-none', 'border-tint-subtle', - 'py-5', + 'pt-5', 'first:pt-0', 'xl:max-2xl:page-api-block:p-5', 'empty:hidden' @@ -275,7 +272,7 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) { 'sticky bottom-0 z-10 mt-auto flex flex-col', 'bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle', 'border-tint-subtle xl:max-2xl:page-api-block:border-t xl:max-2xl:page-api-block:p-2', - 'py-4' + 'pt-4' )} > {/* Mode Switcher */} diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx index 4d3dc0e6e..0992b722f 100644 --- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx +++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx @@ -1,5 +1,4 @@ 'use client'; -import { motion } from 'framer-motion'; import React from 'react'; import { useScrollActiveId } from '@/components/hooks'; @@ -15,6 +14,11 @@ import { AsideSectionHighlight } from './AsideSectionHighlight'; */ const SECTION_INTERSECTING_THRESHOLD = 0.9; +/** + * The offset from the top of the page when scrolling to the active item. + */ +const ACTIVE_ITEM_OFFSET = 100; + const springCurve = { type: 'spring', stiffness: 700, @@ -39,10 +43,25 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) { enabled, }); + const scrollContainerRef = React.useRef(null); + const activeItemRef = React.useRef(null); + + React.useEffect(() => { + if (activeId && activeItemRef.current && scrollContainerRef.current) { + scrollContainerRef.current?.scrollTo({ + top: activeItemRef.current.offsetTop - ACTIVE_ITEM_OFFSET, + behavior: 'smooth', + }); + } + }, [activeId]); + return ( -
    +
      {sections.map((section) => ( - 1 && ['ml-3', 'my-0', 'sidebar-list-line:ml-0'] )} + ref={activeId === section.id ? activeItemRef : null} > {activeId === section.id ? ( - + ))}
    );