22 KiB
Local nav & outline chrome
Part of the theme chrome audit · snapshot 2026-08-24 · baselined on #5397 (navbar-redesign)
Already solved by #5397
- #2329 — feat(theme): use inert to avoid traverse menus and content with keyboard (kind: pr-unmerged; demand: 👍0, 0c)
- Moves
isScreenOpenout of the composable closure and appliesinertinLayout.vueso the covered page (including the local nav) is unreachable by keyboard while the mobile screen is open. - #5397 lands exactly this, same files:
nav.tshoistsisScreenOpen/screenTriggerElto module scope, andLayout.vuepasses:inert="isScreenOpen"toVPLocalNav,VPSidebar,VPContent,VPFooterandVPSkipLink(which gained an explicitinertprop because it has two root nodes).
- Moves
Fits a planned follow-up
-
#4359 — Local navigation dropdown misplaced without sidebar (kind: issue-open; demand: 👍0, 5c)
- The outline dropdown panel is offset by the sidebar width even on pages with no sidebar, so it floats off-position. Maintainer's stated fix in-thread:
--vp-sidebar-widthshould be 0 when there is no sidebar. - Local-nav/sidebar rework. Still unfixed on
navbar-redesign:VPLocalNavOutlineDropdown.vue:172keepsleft: calc(var(--vp-sidebar-width) + 2rem)unconditionally at@media (min-width: 60rem), andstyles/vars.css:541defines--vp-sidebar-width: 17remas a static token that is never zeroed. The rework must make the sidebar column width a state-driven variable (0 whenhasSidebaris false) so the dropdown, and every other consumer, positions from one source of truth instead of per-component overrides.
- The outline dropdown panel is offset by the sidebar width even on pages with no sidebar, so it floats off-position. Maintainer's stated fix in-thread:
-
#4393 — fix(components): Local navigation location error (kind: pr-open; demand: 👍0, 16c)
- Community fix for #4359: adds a
has-sidebarclass toVPLocalNavOutlineDropdownand a:not(.has-sidebar) .items { left: 32px }override. Approved by a contributor, stalled awaiting a maintainer for months. - Local-nav/sidebar rework. The rework must absorb the outcome but not the mechanism — this PR adds a second hard-coded
leftand a duplicateuseSidebar()call inside the dropdown, which is the exact per-component-override pattern #5397 removed from the navbar. Implement at the variable level (zeroed--vp-sidebar-width) and credit/close this PR.
- Community fix for #4359: adds a
-
#5090 — Theme suggestion: mobile TOC active highlight (kind: issue-open; demand: 👍0, 0c)
- The local nav's "On this page" dropdown renders the outline with no active-heading highlight, unlike the desktop aside.
- Local-nav/sidebar rework.
useActiveAnchoris currently single-consumer: it is called only fromVPDocAsideOutline.vue:16and binds one container plus one marker element. The rework must make active-anchor tracking support two simultaneous consumers (aside outline and local-nav dropdown), so the dropdown gets.activeand a marker without a parallel implementation.
-
#5091 — feat: mobile TOC active highlight (kind: pr-open; demand: 👍0, 0c)
- Implements #5090 by adding a second composable,
useFloatActiveAnchor(items, marker, open), plus a duplicated.outline-markerand.outline-link.activeruleset inside the dropdown. - Local-nav/sidebar rework. This PR is the evidence for the requirement above: the fork exists only because
useActiveAnchorcannot serve a second, conditionally-mounted container. Rework should generalize the one composable and delete the need foruseFloatActiveAnchor.
- Implements #5090 by adding a second composable,
-
#3392 — feat(theme): add 'inert' attribute to prevent unnecessary traversal of hidden content (kind: pr-open; demand: 👍0, 1c)
- Successor to #2932: global inert controls exported from the default theme, plus a focus trap in the "On this page" popup. Touches
VPLocalNavOutlineDropdown.vue,Layout.vue,nav.ts,sidebar.ts,VPSkipLink.vue. - Local-nav/sidebar rework. #5397 took only the screen/Layout inert half. The unabsorbed delta is the local-nav dropdown itself: on the branch it has Escape and
useBodyScrollLockbut no focus trap, no focus return to the trigger on close, and no inert on the page behind it — even though it renders a near-full-height panel. The rework must give the dropdown the same disclosure contract #5397 gave the navbar flyouts (focus containment while open, restore focus to the trigger on Escape/dismiss), and should also export the inert state so custom themes can reuse it.
- Successor to #2932: global inert controls exported from the default theme, plus a focus trap in the "On this page" popup. Touches
-
#3811 — 'Return to top' button is always visible in custom layout page (kind: issue-open; demand: 👍0, 0c)
- With
layout: foo+navbar: false+sidebar: false, the local nav renders as a bare "Return to top" bar permanently, even at scroll top. - Local-nav/sidebar rework, specifically the nav-height DOM probe. Two concrete defects survive on the branch. (1) The probe in
VPLocalNav.vue:24-32doesdocument.body.appendChild(probe)withheight: var(--vp-nav-height), but the--vp-nav-height: 0pxoverride lives on.hide-nav(vars.css:520-522), which is on the layout div, notbody— so the probe always resolves the:rootvalue of4remand theisScrolledgate is wrong whenevernavbar: false. (2) The render gate!isHome && (hasLocalNav || hasSidebar || isScrolled)has no notion of custom layouts, so any non-homelayout gets theempty+fixedlocal nav. The rework must replace the probe with a declared--vp-local-nav-heightvariable resolved in the right cascade scope, and gate rendering on layout type rather than raw scroll position.
- With
-
#2320 — fix(theme): 'Return to top' button is always visible in the home page (kind: pr-unmerged; demand: 👍0, 2c)
- Earlier attempt at the same defect class (home-page variant, for #2312), patching
VPLocalNav.vueandsidebar.tstogether. - Local-nav/sidebar rework. Its value is the precedent that the visibility decision cannot live in the raw
y >= navHeightcomparison and must be co-derived with sidebar/layout state — the same coupling the rework is unifying. The home case was fixed since; the custom-layout case (#3811) was not.
- Earlier attempt at the same defect class (home-page variant, for #2312), patching
-
#2071 — Add support of Global Notification (kind: issue-open; demand: 👍0, 5c)
- Asks for a built-in dismissible banner. Reopened by a maintainer "for docs", then argued by a contributor (xsjcTony) that the slot approach is structurally broken.
- Local-nav/sidebar rework — this is the
--vp-layout-top-heightrequirement. The thread documents exactly what the rework must own: with thelayout-topslot, users must hand-maintain a media-query-matched banner height, inject a<head>script to avoid a dismissal flash on reload, and still get CLS because the fixed navbar andVPLocalNav(padding-top: var(--vp-layout-top-height, 0px),VPLocalNav.vue:75) are offset by a JS-set variable. Requirement: the layout-top offset must be resolved in SSR output, not assigned by script after hydration, and the local nav's sticky origin must follow it without a magic constant. Cited real-world breakage: oxc.rs shows the banner flash.
-
#2334 — Using slot doc-top messes up active heading determination in aside (kind: issue-open; demand: 👍0, 0c)
- Content injected into
doc-topshifts headings, so the active-heading calculation picks the wrong one. Reporter asks that it not depend on hard-coded constants. - Local-nav/sidebar rework. Partly addressed already — the old
__PAGE_OFFSET__is gone andoutline.ts:165-166now reads per-headerscrollMarginTop. What remains is a local-nav-owned magic number:styles/components/vp-doc.css:7,13bakes2.9375rem(the local nav's height) into every heading'sscroll-margin-topbelow80rem, and assumes the local nav is present there regardless of whether it actually renders. There is no--vp-local-nav-heightvariable anywhere in the theme. The rework must introduce one and have bothvp-doc.cssand the runtime probe consume it, so scroll-margin tracks the chrome that is actually on screen.
- Content injected into
-
#4940 — fix(theme): skip link jumps to aside instead main content heading/anchor (kind: pr-open; demand: 👍0, 2c)
- "Skip to content" resolves into the aside/outline rather than the main content heading; PR retargets to the first
h1inside#VPContent mainand makes the skip anchorposition: fixed. - Local-nav/sidebar rework. #5397 touched
VPSkipLink.vuebut only to add theinertprop —href="#VPContent"is unchanged, so the bug ships. The rework must define the skip target relative to main content, and decide the skip link's relationship to the sticky local nav (aposition: fixedskip link and a sticky sub-bar compete for the same top-of-page region).
- "Skip to content" resolves into the aside/outline rather than the main content heading; PR retargets to the first
-
#3773 — '#nav-screen-content-after' does not work when screen width between [768,1280) (kind: issue-not-planned; demand: 👍0, 8c)
- Neither
nav-screen-content-afternornav-bar-content-aftergives the user a place to put extra nav content in the tablet band; reporter wants it folded into a popup menu there. - Local-nav/sidebar rework. The decline reason was structural, not a rejection of the need: in [768,1280) there is no nav screen to append to and the bar had no overflow container. #5397 removed half that premise by giving the bar a real
⋯overflow menu at any width. The rework must define where page-level extra nav content lives in the band where the local nav is the primary chrome, and make the slot contract explicit rather than silently rendering nothing.
- Neither
-
#4953 — Show "On this page" sidebar more often in <1280px width (kind: issue-not-planned; demand: 👍0, 2c)
- Asks for the aside outline below 1280px, showing that trimming
VPSidebarpadding 32→24px and droppingVPDoc .contenthorizontal padding frees the needed 80px at 1200px. - Local-nav/sidebar rework. The decline rested on two claims, and the rework invalidates the load-bearing one: "no community interest" plus "even in smaller viewports, the on this page section is still expandable" — i.e. the local nav dropdown is the accepted substitute for the aside in this band. That makes it the rework's obligation to make the dropdown an adequate substitute (active highlight per #5090, correct positioning per #4359, focus handling per #3392). If the rework also revisits the 960/1280 band for #4897, the padding budget in this issue is concrete input.
- Asks for the aside outline below 1280px, showing that trimming
-
#1764 — Navbar is not sticky in mobile breakpoint (kind: issue-not-planned; demand: 👍1, 2c)
- The primary navbar is not sticky at mobile widths; only the secondary (local) nav is. Reporter notes the home layout has no secondary nav, so nothing is sticky there.
- Local-nav/sidebar rework. Declined as "a design choice — only the secondary nav is sticky", which is precisely the contract the rework is redefining. The unmet case is concrete and survives: on
isHomeand on any page whereVPLocalNavdoes not render, the "secondary nav is the sticky one" rule leaves the user with no sticky chrome at all. The rework must state which element owns stickiness per layout, rather than leaving it implicit in the local nav's render gate.
-
(delta on known #4897) (kind: issue-open; demand: 👍0, 5c)
- Thread carries a concrete SSR mechanism the PR body does not record.
- The PR body says only "SSR determinism for the local nav (#4897)". The thread adds: brc-dd proposes stamping a class on
<html>at build time, conditional on the SSR'd HTML actually containing header anchors and outline/aside not being disabled in frontmatter/themeConfig — i.e. decide the local nav's presence from build output rather than on hydration. sapphi-red adds a hard constraint the rework must not break: header-less pages must end up with no local nav, and he was unable to remove it on hydration for/guide/mpa-mode(a real page with no headers). brc-dd also signals MPA mode is acceptable collateral. Record both the mechanism and the header-less-page test case.
-
(delta on known #3393) (kind: issue-not-planned; demand: 👍0, 5c)
- Thread contains a requirement and a constraint the PR body's "the sidebar curtain" bullet does not capture.
- Requirement: giladgd needs the curtain back as an opt-in flag, not a CSS recipe, and demonstrates why — it needs an extra DOM element plus the logic built around it, so a pure-CSS override cannot reproduce it (he is pinned to an old VitePress version over this). Constraint from elringus, which the rework's implementation must satisfy: the old curtain intercepted pointer events over visibly-unobscured links, so a restored curtain must be pointer-events-transparent. bluwy's "can't please everyone with different designs" is the standing counterweight, which an opt-in flag resolves rather than fights.
New candidates for future rework
-
#3534 — breadcrumb (kind: issue-open; demand: 👍7, 11c)
- Requests a built-in breadcrumb trail above the page title. Highest 👍 count in this area and unaddressed anywhere in #5397's plan.
- A rework of page-level nav chrome would incorporate it as the natural third element of the local nav / doc-top region, alongside the "Menu" trigger and the outline dropdown. The blocker recorded in-thread is data, not layout: brc-dd notes only the leaf can be inferred from the title, and peterbe's case (markdown copied in from another repo, no controllable frontmatter) rules out per-file frontmatter. So the requirement is deriving the ancestor path from the resolved sidebar structure, with frontmatter as an override — the same sidebar resolution the rework already has in hand.
-
#4521 — Scroll to top button on desktop (kind: issue-open; demand: 👍4, 3c)
- Wants a go-to-top affordance on long pages at desktop widths. Commenters note the mobile "Go to top" is not visible to them either.
- A rework would treat this as parity, not a new feature: the local nav already ships "Return to top" as the dropdown's empty-state button, so the question is where that same action lives once the local nav is absent (≥80rem). Natural home is the aside outline footer, sharing the local nav's
scrollToTopand thereturnToTopLabelstring rather than adding a second label and handler.
-
#4522 — feat: add scroll to top button in VPDocAsideOutline component (kind: pr-unmerged; demand: 👍3, 0c)
- 16-line implementation of #4521 in
VPDocAsideOutline.vue. Closed without discussion. - Shows the minimal shape but hard-codes its own button and copy. A rework should hoist the return-to-top control into a shared piece used by both the local nav dropdown and the aside, so the two surfaces cannot drift.
- 16-line implementation of #4521 in
-
#2146 — Auto expand/collapse sections in page outline (kind: issue-open; demand: 👍8, 2c)
- Deeply nested outlines are hard to scan; asks that child headings expand only while their parent is active. Includes light/dark mockups of the target behavior.
- Highest-demand outline behavior request. It interacts with the local nav because
VPDocOutlineItemis shared verbatim by the dropdown, where vertical space is scarcest (the panel is height-capped by--vp-vh) — so collapsing benefits the dropdown more than the aside. A rework that generalizes active-anchor tracking to two consumers (see #5090/#5091) is the same change that makes "expand the active branch" implementable in both.
-
#2297 — fix: better .has-aside condition (kind: pr-unmerged; demand: 👍0, 0c)
- Recomputes
.has-asidefrom rendered slots,theme.carbonAds,getHeaders()and thelayout/asideconfigs, with unit tests for slot detection. - Relevant because
hasAsideandhasLocalNavare decided by different rules today (layout.tsderiveshasAsidefrom config only,hasLocalNavfromheaders.length), so the two can disagree about whether a page has an outline at all. A rework should make outline presence one derived fact consumed by the aside, the local nav and the band handoff at 960/1280px; this PR's slot-and-config detection is the prior art for that predicate.
- Recomputes
Not viable
- #4735 — feat: add scroll support for the TOC (kind: pr-unmerged; demand: 👍5, 1c) — Shipped differently in #5377 (merged);
outline.ts:199now callsactiveLink.scrollIntoView({ block: 'nearest' }). - #3351 — Keep aside marker visible (kind: issue-open; demand: 👍0, 0c) — Resolved by merged #5377 but never closed. Recommend closing rather than reworking.
- #3387 — Fix: Make the outline follow the page scroll (kind: pr-unmerged; demand: 👍0, 2c) — Superseded by merged #5377.
- #4634 — feat: active outline link scroll to page center (kind: pr-unmerged; demand: 👍0, 3c) — Superseded by merged #5377, which chose
block: 'nearest'over centering. - #4457 — feat(theme): add doc aside scroll spy (kind: pr-unmerged; demand: 👍0, 1c) — Superseded by merged #5377.
- #3901 — feat: Improve Sidebar and Aside Link Visibility on Mount and Route Change (kind: pr-open; demand: 👍0, 0c) — Aside half superseded by merged #5377; remaining half (#3426, scroll active sidebar link into view) is sidebar scope.
- #2189 — fix(theme): ensure correct outline state (kind: pr-unmerged; demand: 👍0, 1c) — Both symptoms gone from current
outline.ts; bluwy closed as "most of the issue explained is now fixed". - #2676 — fix(theme): outline marker flicks when navigating towards above (kind: pr-unmerged; demand: 👍0, 3c) — Targets an activation path that no longer exists; marker positioning rewritten (
outline.ts:190-201). - #1631 — feat: add Set custom menu/return to top labels (kind: pr-unmerged; demand: 👍0, 1c) — Already shipped:
sidebarMenuLabelandreturnToTopLabellive. - #2455 — fix(theme): nav bar overflowed by aside when no sidebar (kind: pr-unmerged; demand: 👍0, 7c) — Its issue #2442 closed as completed; #5397 rebuilt the geometry it patched.
- #1916 — fix 1915: aside always rendered even when outline is false (kind: pr-unmerged; demand: 👍0, 1c) — Issue #1915 closed as completed; handling in
layout.tstoday. - #3194 — TOC aside height problem (kind: issue-open; demand: 👍0, 0c) — Aside-only positioning complaint; no local-nav interaction.
- #5074 — feat(theme): add active link on mobile menu (kind: pr-unmerged; demand: 👍0, 2c) — Patches
VPNavScreenMenuLink.vue, which #5397 deletes; #5068 addressed on main by0f0fe135. - #1145 — Dynamic Outline (kind: issue-not-planned; demand: 👍0, 6c) — Markdown/build-time concern, out of area.
- #2134 — Aside Location Order (kind: issue-not-planned; demand: 👍0, 3c) — Already supported via
aside: 'left'.