ENH: Implement sticky RHS table of contents with scroll highlighting#350
ENH: Implement sticky RHS table of contents with scroll highlighting#350
Conversation
- Add sticky_contents config option (defaults to False) - Use position: fixed for reliable sticky behavior - Add scrollspy.js module for tracking scroll position - Highlight active section in TOC as user scrolls - Support scrollable TOC for long content lists Fixes #133 Usage: html_theme_options: sticky_contents: true
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #350 +/- ##
=======================================
Coverage ? 45.92%
=======================================
Files ? 2
Lines ? 405
Branches ? 0
=======================================
Hits ? 186
Misses ? 219
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
🎭 Visual Regression Test ResultsDetails
Skipped testsmobile-chrome › theme.spec.ts › Theme Features › f-string interpolation styling |
- Shows floating button at bottom right when page is scrolled down - Only appears when sticky_contents is enabled - Uses modern Sass color.adjust() function - Animated appearance with fade and slide transition - Button hidden until user scrolls past 300px threshold
- Subsections are hidden by default in sticky TOC mode - When a section becomes active, its parent sections expand - When scrolling away, subsections collapse back - Smooth animated transitions for expand/collapse - Only affects sticky mode - non-sticky TOC unchanged
- Subsections now stay expanded while scrolling through content - Parent section stays expanded when any child is active - Also expands parent's children when parent heading is active - Only collapses when scrolling to a different top-level section
- Track which top-level TOC section each item belongs to - When active item changes, keep its entire top-level section expanded - Expand ALL nested items within the active top-level section - Subsections only collapse when scrolling to a different top-level section - Uses :has() selector for finding items with children
- Fix parent traversal to correctly find top-level TOC item - Replace :has() selector with :scope > ul for broader browser support - Ensure all nested items with children get expanded class
- New theme option: contents_autoexpand (default: false)
- When false: TOC shows all sections normally (no collapse/expand)
- When true: Subsections auto-collapse/expand based on scroll position
- Allows sticky TOC to ship while auto-expand is refined
- Uses data-autoexpand attribute to pass config to JavaScript
Usage in _config.yml:
html_theme_options:
sticky_contents: true
contents_autoexpand: true # optional, defaults to false
- Expand all ancestors of the active item (so it's visible in collapsed tree) - Expand the active item itself if it has children (show its subsections) - Removed complex top-level tracking - simpler ancestor traversal - When on 4.2, shows 4.2.1, 4.2.2 etc - When on 4.2.1, keeps 4.2 expanded so 4.2.1 is visible
- Start from parent ul of active item, not the item itself - Walk up ul->li->ul->li chain correctly - Ensures parent sections get expanded class when in subsection
- Theme config values come as strings ('True'/'False') not booleans
- Updated conditionals to check for both boolean and string values
- Fixes autoexpand not being enabled despite default True in theme.conf
- Change overflow-y from 'auto' to 'hidden' for cleaner appearance - Content that exceeds viewport height will be clipped at bottom - Also adds Core Rules section to copilot-instructions.md
|
@jstac @DrDrij I have implemented the This implementation is enabled through an option for the theme. Here is a video of the new implementation which keeps the RHS contents visible while you scroll the page, and continuously provides context as you scroll through sections and subsections in the document. It also has a new demo-stick-contents-autoexpand.movInterested in thoughts / comments / suggestions. |
|
Here is demo of demo-autoexapand-false.mov |
- When contents_autoexpand is false, highlight the top-level parent section instead of the hidden subsection as user scrolls through content - Move back-to-top button outside .inner container to prevent clipping from overflow-y: hidden
Functionality look perfect @mmcky. Nothing to add. Just a thought, since you're introducing anchor points, why not make these accessible for users to share links to specific context..
|
|
Thanks @DrDrij. Nice idea, would you add that to Contents or next to the titles themselves (when reading the doc) do you think? |
- Fix test_git_functions_unit being accidentally merged into test_sticky_toc - Move .logo/.powered selectors out of .back-to-top-btn SCSS nesting - Extract repeated Jinja boolean checks into is_sticky/is_autoexpand variables
Changed from a sibling <button> after the <a> to an inline <span> inside the <a>, so the icon flows naturally at the end of the title rather than appearing on a separate line.
- Increase settle time from 500ms to 1000ms for MathJax font metrics - Switch from maxDiffPixels:300 to maxDiffPixelRatio:0.15 to handle font rendering variance across environments (CI reported 0.10 ratio)
Position the icon absolutely 18px right of the <a> link, vertically centered. This prevents the icon from taking up inline space and causing text to wrap to the next line on longer TOC entries.
There was a problem hiding this comment.
Pull request overview
Implements an optional sticky right-hand table of contents for the QuantEcon Book Theme, including scroll-position highlighting (“scroll spy”) and related UI affordances, controlled via new html_theme_options.
Changes:
- Add a new
scrollspy.jsmodule and initialize it from the theme’s main JS entrypoint. - Update theme layout + SCSS to support a fixed-position TOC, active-section styling, copy-link affordance, and a sticky-only back-to-top button.
- Add/adjust tests and documentation for the new theme options (
sticky_contents,contents_autoexpand).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/quantecon_book_theme/assets/scripts/scrollspy.js |
New scroll spy + TOC copy-link/back-to-top behavior for sticky TOC mode. |
src/quantecon_book_theme/assets/scripts/index.js |
Imports/initializes scroll spy on DOM ready. |
src/quantecon_book_theme/assets/styles/_page.scss |
Adds sticky positioning and styling for active TOC items, copy-link icon, and back-to-top button. |
src/quantecon_book_theme/theme/quantecon_book_theme/layout.html |
Conditionally renders sticky TOC container + data attributes and sticky-only back-to-top link. |
src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf |
Introduces default values for sticky_contents and contents_autoexpand. |
tests/test_build.py |
Adds build-time assertions for sticky TOC option behavior and defaults. |
tests/visual/theme.spec.ts |
Makes MathJax screenshot assertions less environment-sensitive. |
docs/configure.md |
Documents configuration and behavior of sticky TOC + auto-expand. |
.github/copilot-instructions.md |
Adds internal workflow rules for CLI output capture, long-running commands, and test/build workflow. |
Comments suppressed due to low confidence (6)
src/quantecon_book_theme/assets/scripts/scrollspy.js:223
- The copy control is a
spanwithrole="button"appended inside an<a>, which nests interactive elements and is not keyboard accessible (notabindex/ key handlers). Consider rendering a real<button type="button">as a sibling to the link (or otherwise avoid nesting) and add keyboard interaction for accessibility.
const icon = document.createElement("span");
icon.className = "toc-copy-link";
icon.setAttribute("role", "button");
icon.setAttribute("aria-label", "Copy link to section");
icon.setAttribute("title", "Copy link to section");
// Use a simple clipboard SVG icon
src/quantecon_book_theme/theme/quantecon_book_theme/layout.html:147
- The
is_sticky/is_autoexpandboolean parsing is verbose and repeated. You can simplify and make it more robust by normalizing to string (e.g.,|string|lower) once and comparing to'true', which will handle both Python booleans and theme.conf string values consistently.
{# Determine sticky TOC and autoexpand settings once #}
{% set is_sticky = theme_sticky_contents is defined and (theme_sticky_contents is sameas true or theme_sticky_contents == "True" or theme_sticky_contents == "true") %}
{% set is_autoexpand = theme_contents_autoexpand is defined and (theme_contents_autoexpand is sameas true or theme_contents_autoexpand == 'True' or theme_contents_autoexpand == 'true') %}
{% if is_sticky %}
<div class="inner sticky" data-autoexpand="{% if is_autoexpand %}true{% else %}false{% endif %}">
{%- else %}
docs/configure.md:234
- In this bullet, “discrete” appears to be used in the sense of “unobtrusive”; the usual word is “discreet”.
- **Back to top button**: A discrete "Back to top" button appears after scrolling down 300px
- **Auto-expand subsections**: Subsections automatically expand to show the current hierarchy
src/quantecon_book_theme/assets/styles/_page.scss:61
.inner.stickysetsoverflow-y: hidden, which will clip long TOCs with no way to scroll/click headings that fall below the viewport. Consider usingoverflow-y: auto(orscroll) and optionally hiding the scrollbar via CSS if desired.
.inner.sticky {
position: fixed;
top: 7rem;
width: 200px;
max-height: calc(100vh - 8rem);
overflow-y: hidden; // Clip content at bottom, no scrollbar
}
src/quantecon_book_theme/assets/styles/_page.scss:113
- The copy-link icon is hidden via
opacity: 0but will still receive pointer events, which can make clicks near the right edge unexpectedly trigger a copy instead of following the TOC link. Consider disabling pointer events (or usingvisibility: hidden) when hidden and enabling them on hover.
.toc-copy-link {
position: absolute;
right: -18px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: colors.$body;
opacity: 0;
transition: opacity 0.15s ease, color 0.15s ease;
src/quantecon_book_theme/assets/scripts/scrollspy.js:247
navigator.clipboard.writeText(...)is called without checking for Clipboard API support and without a.catch(...). In non-secure contexts or when permissions are denied, this can throw or create an unhandled rejection. Consider feature-detectingnavigator.clipboard?.writeText, adding error handling, and providing a fallback (or at least no-op gracefully).
const href = link.getAttribute("href");
const url =
window.location.origin + window.location.pathname + href;
navigator.clipboard.writeText(url).then(() => {
// Show brief "Copied!" feedback
icon.classList.add("copied");
const originalTitle = icon.getAttribute("title");
icon.setAttribute("title", "Copied!");
setTimeout(() => {
icon.classList.remove("copied");
icon.setAttribute("title", originalTitle);
}, 1500);
});
The absolute positioning pushed the icon outside the 200px container with overflow:hidden, making it invisible. Instead, use display: inline-block with width:0 and overflow:visible — the icon visually appears after the text via margin-left on the SVG, but takes zero width in the flow so it can never cause a line break.
|
thanks @DrDrij -- I have implemented your suggestion. Looks nice.
|



Summary
This PR implements a sticky right-hand side table of contents (TOC) that remains fixed while scrolling, highlights the currently active section, and provides several quality-of-life features for navigating long pages.
Fixes
Features
contents_autoexpand)sticky_contents: trueTechnical Approach
position: fixedinstead ofposition: stickyfor reliable behavior regardless of parent container positioningrequestAnimationFramefor throttled scroll handling (smooth performance)scrollspy.jsmoduledisplay: inline-block; width: 0; overflow: visible) to appear after TOC text without affecting layout or causing line wrappingnavigator.clipboard.writeText) with visual "Copied!" feedbackConfiguration
Add to your Jupyter Book
_config.yml:Or in Sphinx
conf.py:When
contents_autoexpandisFalse, only top-level sections are shown with bold highlighting for the active section — providing a cleaner, more compact TOC.Files Changed
src/quantecon_book_theme/assets/scripts/scrollspy.js(NEW) — Scroll spy + copy-to-clipboard functionalitysrc/quantecon_book_theme/assets/scripts/index.js— Import and initialize scrollspysrc/quantecon_book_theme/assets/styles/_page.scss— Sticky positioning, active highlights, auto-expand/collapse, back-to-top button, copy-link icon stylessrc/quantecon_book_theme/theme/quantecon_book_theme/layout.html— Conditional sticky class withdata-autoexpandattribute, back-to-top buttonsrc/quantecon_book_theme/theme/quantecon_book_theme/theme.conf— Newsticky_contentsandcontents_autoexpandoptionsdocs/configure.md— Full documentation for the Sticky TOC featuretests/test_build.py— Unit tests for sticky TOC configurationTesting
sticky_contents: truein lecture-python-programming.myst