Skip to content

The contents drawer loads closed on every page, with no counterpart to the book theme's persistent_sidebar #214

Description

@mmcky

Problem

On the MyST lecture sites the contents drawer loads closed on every page. A reader who opens it to move between lectures has to reopen it after each link, and within a page it closes at the first click or text selection outside it. The book theme's persistent_sidebar option keeps its sidebar open across pages on screens wider than 1340px. This theme has no counterpart, and no issue tracks one.

Why it loads closed, at quantecon-theme.mystmd d504555:

  • The open state is the browser's popover state and nothing else. The drawer is popover="auto" with no React state (ContentsSidebar.tsx L113-L117), and the toggle has no handler and no state by design (SidebarToggle.tsx L4-L11). The only browser-storage use under app/ is the colour theme's (Document.tsx L85-L92).
  • Static builds render every internal link with reloadDocument (Document.tsx L74-L78), switched on by MODE === 'static' (root.tsx L236-L237). Every navigation is a full page load, so the popover starts closed.
  • The drawer is light-dismissed at every width, because popover="auto" is set with no width condition. Escape, or a click or text selection anywhere outside it, closes it, "where before it stayed open until toggled" (CHANGELOG.md L338-L341).
  • No option changes this. The layout options are only hide_toc and hide_search (template.yml L99-L105), and the CLI drops any site option the template does not declare (template.yml L23-L25). A persistent_sidebar copied into myst.yml would be discarded.
  • docs/layout.md L14-L19 describes the drawer without mentioning persistence, although docs/index.md L42-L45 promises that where a Sphinx option has no counterpart, its page says so and why.

What the book theme does, at quantecon-book-theme 649cc37:

Book theme with persistent_sidebar: true This theme
Option Defaults to False (theme.conf L37). Only a real boolean true adds the persistent class (layout.html L367-L371). None
Stored state Every open and every close writes localStorage.setSidebar (sidebar.js L10-L26). Nothing stored
Page load On DOMContentLoaded (index.js L24-L33), reopens when the stored flag is 1, the class is present and the window is wider than 1340px (sidebar.js L28-L40). Loads closed
Click outside A close-on-click handler is bound only when the toggle is clicked at 1340px or narrower (sidebar.js L43-L59), so a sidebar opened wider, or restored on load, stays open. Closes the drawer at every width

Scale: one lecture site, not yet on this theme. lecture-datascience.myst sets persistent_sidebar: true (lectures/_config.yml L70). It pins quantecon-book-theme 0.8.0 (environment.yml L11), which has the same restore logic (index.js L114-L142 at v0.8.0). A code search of the QuantEcon organisation for persistent_sidebar on 2026-09-11 found no other lecture repository that sets it. The site still builds with the Sphinx theme and is repointed to this one when it cuts over (PLAN.md L195-L198), and its readers lose the behaviour at that point.

PR #144, the Popover API rebuild, set persistence aside rather than deciding against it. Its 2026-08-25 review noted that light dismiss "ends its old role as a persistent side map while reading — including on 2xl viewports where the 350px panel overlaps nothing". The 2026-09-03 decisions comment accepted popover="auto": "If persistence turns out to matter on wide screens it is a one-attribute change plus an Escape handler, so it can be revisited on the demo site rather than here." It also found that the open state was already lost across navigation before the rebuild, and said "if persistence across navigation is wanted it is a follow-up issue, not a condition on this PR". Both statements are conditional, and persistence is not on that comment's own "Not done here, for follow-up" list. No such issue exists. On 2026-09-11, searching this repository's issues for "persistent sidebar" and "persistent_sidebar" finds only #33 (closed in June 2026). That issue's comparison table marked Persistent Sidebar as Missing, and its recommendation 5 proposed an option to keep the sidebar visible.

Proposed change

Add an opt-in counterpart to persistent_sidebar. Leave the default behaviour as it is.

  1. Declare a boolean persistent_sidebar site option in template.yml, so lecture-datascience.myst can carry its setting across unchanged. Give it no default:, for the reason given at template.yml L31-L33. Document it next to hide_toc in docs/configuration.md L26-L27.
  2. When the option is set, store the drawer's open or closed state in browser storage whenever the popover toggles, with every read and write guarded. On page load, reopen the drawer if the stored state is open and the viewport is wide enough that the drawer does not cover the reading column. The book theme's threshold is 1340px. The refactor: rebuild the contents drawer on the Popover API #144 review pointed to 2xl (1536px), where the drawer is 350px wide (styles/app.css L118-L122). The PR should settle which width to use.
  3. At those widths, a restored drawer has to stay open while the reader works in the article, because under popover="auto" the first click there closes it. This reopens the refactor: rebuild the contents drawer on the Popover API #144 light-dismiss decision, but only for sites that opt in and only on wide viewports, which that decision left open to revisit. The refactor: rebuild the contents drawer on the Popover API #144 review and decision describe the change: popover="manual" there, plus an Escape handler. Keep the existing hook that hides the drawer when a dialog opens (ContentsSidebar.tsx L44-L76).
  4. Two details to settle in the PR:
    • Opening the popover on load plays its @starting-style slide-in (styles/app.css L133-L137). Decide whether a restored drawer slides in after load or appears already open. The book theme's restore removes .inactive, which is animated by a 0.2s transform transition (_sidebar.scss L38-L40, L61-L65).
    • The dialog hook closes the drawer when search opens. Decide whether that close is stored. If it is, a reader who jumps to a lecture through search lands with the drawer closed. In the book theme, no script outside sidebar.js touches the sidebar state.

If the maintainers decide against the option, docs/layout.md should instead name persistent_sidebar as having no counterpart and say why, as docs/index.md promises.

Acceptance criteria

  • persistent_sidebar (boolean) is declared in template.yml and documented in docs/configuration.md, and a value set in myst.yml reaches the theme.
  • With the option set, on a viewport above the chosen width: after opening the drawer and following a link in it, the next page loads with the drawer open. After closing it and reloading, the page loads with it closed.
  • With the option set, on those viewports a click or text selection in the article leaves the drawer open. The toggle, Escape and opening search still close it.
  • Below the chosen width, the drawer loads closed and is still light-dismissed, whatever the stored state.
  • With the option unset, behaviour is unchanged and nothing is written to browser storage.
  • When a storage read or write throws, the new code catches it and the drawer loads closed. Whole-page hydration with storage blocked is outside this change: the upstream colour-theme hook reads localStorage without a guard (@myst-theme/site 1.3.0, src/hooks/theme.tsx line 57).
  • A Playwright test covers restoring the drawer after navigation with the option set, and the chosen behaviour after search closes the drawer. drawer-opens-without-javascript, drawer-closes-when-search-opens and npm run test:fouc still pass.
  • The "Contents drawer" section of docs/layout.md describes the option and the width it applies at.

Context

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement to existing material

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions