Updated: March 17, 2026
- Overview
- The Problem
- The Solution
- Core Concepts
- Architecture
- Common Scenarios
- Implementation Details
- Debugging Guide
- See Also
The scroll orchestration system coordinates TanStack Virtual lists in both panes. Each pane tracks structural changes, defers scroll execution until data and DOM state are ready, and applies deterministic alignment for user-driven actions.
Primary implementation:
src/hooks/useNavigationPaneScroll.tssrc/hooks/useListPaneScroll.ts- Support modules:
src/types/scroll.ts - Navigation-specific index resolution:
src/utils/navigationIndex.ts
Virtual lists rebuild whenever folder trees, list contents, or settings change. A rebuild invalidates cached indices while scroll requests are still pending, so naive scrolling lands on the wrong item or fails silently.
- User has tag "todo" selected at index 61.
- User toggles "Show hidden items".
- Hidden tag "archived" becomes visible at index 40.
- Tree rebuilds, "todo" is now at index 62.
- Without orchestration: scroll uses stale index 61 and targets the wrong tag.
- With orchestration: scroll waits for the rebuild, resolves the tag again, and hits index 62.
This kind of shift happens with visibility toggles, layout changes, sorting updates, folder/tag/property navigation, and asynchronous metadata hydration.
Scroll orchestration combines version tracking, intent metadata, and priority coalescing.
graph LR
A[User Action] --> B[Tree/List Change]
B --> C[Index Version++]
A --> D[Scroll Request]
D --> E[Set minVersion]
E --> F{Container Ready + Version Met}
F -->|No| G[Wait]
F -->|Yes| H[Resolve Index]
H --> I[Execute Scroll]
G --> F
Key principles:
- Always resolve indices at execution time.
- Increment pane-specific versions when index maps change.
- Block scroll execution until the pane reports visible and the required version is reached.
- Use intent metadata to pick alignment and (in the list pane) replace lower-priority requests.
Both panes maintain indexVersionRef counters.
- Navigation pane increments when the
pathToIndexmap changes size or identity. - List pane increments when the
filePathToIndexmap changes size or identity and triggersrowVirtualizer.measure()so item heights stay current.
These counters allow pending scrolls to specify the version they require before execution.
Each pane stores at most one pending request.
- Navigation pane records
{ path, itemType, intent, align?, minIndexVersion? }. Paths are normalized by item type. - List pane records
{ type: 'file' | 'top', filePath?, reason?, minIndexVersion? }.
A pendingScrollVersion state value forces React effects to re-run whenever a new request replaces the previous one.
Intent metadata ties each request to its trigger and alignment policy.
- Navigation intents:
selectionis used for immediate selection scrolling (not stored as a pending scroll). Pending scrolls currently usevisibilityToggle,external, andmobile-visibility.startupandrevealexist in the type but are not currently enqueued byuseNavigationPaneScroll. - List intents:
folder-navigation,visibility-change,reveal,list-structure-change.'top'requests use the same priority system withtype: 'top'.
Scroll execution runs inside an effect that checks pane readiness and index versions.
if (!pending || !isScrollContainerReady) {
return;
}
const requiredVersion = pending.minIndexVersion ?? indexVersionRef.current;
if (indexVersionRef.current < requiredVersion) {
return;
}
const index = resolveIndex(pending);
if (index >= 0) {
virtualizer.scrollToIndex(index, { align: getAlign(pending.intent) });
pendingScrollRef.current = null;
}Navigation resolves via getNavigationIndex. List scrolls either use scrollToIndex or scrollToOffset(0) for top
requests.
- Both hooks use
ResizeObserverto detect when the DOM container has width and height. Scrolls never run while the container or any parent is hidden. - The composed
isScrollContainerReadyflag requires both logical visibility and physical dimensions. - Both panes use TanStack Virtual
scrollMarginto align row math with pane chrome. The list pane also usesscrollPaddingStart, and both panes can usescrollPaddingEndfor bottom overlays. - In the list pane, mobile taps on the pane header call
handleScrollToTop, which performs a smoothscrollTo({ top: 0 }).
useNavigationPaneScroll wires TanStack Virtual for navigation items (folders, tags, properties, shortcuts, recent notes, and
spacers).
- Virtualizer setup: Item height estimates follow navigation settings and mobile overrides.
scrollMarginaligns virtualization math andscrollToIndexbelow the unpinned banner, andscrollPaddingEndis used when bottom overlays need extra space. - Safe viewport adjustment:
scrollToIndexSafelyrunsscrollToIndex, thenensureIndexNotCoveredapplies a bottom-overlap correction whenscrollPaddingEndis non-zero. Top alignment is handled byscrollMargin. - Selection handling: The hook watches folder/tag/property selection, pane focus, and visibility. It suppresses auto-scroll
when a shortcut is active or when
skipAutoScrollis enabled for shortcut reveals. - Hidden item toggles: When
showHiddenItemschanges, the current selection is queued with intentvisibilityToggleandminIndexVersion = current + 1. - Tag/property selection: Tag and property rows can load after folders, so selection scrolling for these types is handled in a dedicated effect.
- Pending execution: While a visibility toggle is in progress, only
visibilityTogglerequests can execute. After running such a scroll, the hook rechecks the index on the next animation frame and queues a follow-up if the index moved again. - External entries:
requestScrollnormalizes the path and queues anexternalintent so reveal flows can drive the navigation pane. - Mobile drawer: A
notebook-navigator-visibleevent first attempts an immediate scroll for the current selection. If the pane is not ready or the row index is not available yet, the hook queues amobile-visibilitypending scroll. - Settings changes: Navigation sizing changes trigger
measure()and anautoscroll to keep the selection within the safe viewport. Scroll inset changes (scrollMargin,scrollPaddingEnd) use the sameautoscroll path.
useListPaneScroll manages article lists, pinned groups, spacers, and date headers.
- Virtualizer setup: Height estimation mirrors
FileItemlogic, looking up preview availability synchronously and respecting compact mode.scrollMarginandscrollPaddingStart/scrollPaddingEndkeep scroll math aligned with overlay chrome and the mobile bottom toolbar. - Priority queue:
setPendingwrapsrankListPending, replacing lower-ranked requests. - Selected file tracking:
selectedFilePathRefavoids executing stale config scrolls for files that are no longer selected. - Selection index resolution:
getSelectionIndexreturns the header index for the first file in the list when a header exists directly above it; otherwise it returns the file index. - Context tracking:
contextIndexVersionRefmaintains the last version seen per folder/tag/property context. When the index advances within a folder, tag, or property context (pin/unpin, reorder, delete), the hook queues alist-structure-changescroll (whenrevealFileOnListChangesis enabled) so the selected file remains visible. - Folder navigation: When the list context changes or
isFolderNavigationis true, the hook clears stale pending work, then sets a pending request (file or top) and clears the navigation flag. Pending entries can be queued even when the pane is hidden and execute once the list becomes ready. - Reveal operations: Reveal flows queue a
revealpending scroll. Startup reveals override alignment to'center'. - Mobile drawer: The
notebook-navigator-visibleevent queues a visibility-change scroll when a file is selected. - Settings and search: Appearance changes and descendant toggles queue
list-structure-changeentries. Search filters queue atopscroll when the selected file drops out of the filtered list, respecting mobile suppression flags. - Dynamic height updates: The hook re-measures the list when list indices change and when the in-memory database reports preview, feature image, tag, metadata, or property updates.
Toggling Hidden Items
showHiddenItemsflips in UX preferences.- The navigation hook queues the current selection with
intent: 'visibilityToggle'andminIndexVersion = current + 1, then waits for the nextpathToIndexrebuild to advanceindexVersion. - After the tree rebuild, the pending scroll resolves the selection and runs.
- The stabilization check revalidates the index on the next frame and queues another scroll if the index moved again.
- Selection context raises
isFolderNavigationwhen the user picks a folder, tag, or property. useListPaneScrollqueues a file or top scroll with reasonfolder-navigationand clears the flag.getListAligncenters the selection on mobile and usesautoon desktop.- The navigation pane independently scrolls to the selected folder/tag/property row when the pane becomes focused or visible.
- Navigation line height or indentation updates trigger
rowVirtualizer.measure()followed by a deferred selection scroll when auto-scroll is allowed. - List appearance or descendant toggles queue a
list-structure-changescroll withminIndexVersion = current + 1whenrevealFileOnListChangesis enabled and a file is selected. Disabling descendants can queue atopscroll when no file is selected orrevealFileOnListChangesis disabled. - Reorders within the same folder/tag/property context update
indexVersionand enqueue alist-structure-changescroll so the selected file remains visible.
- Reveal flows call
requestScrollfor the navigation pane and setselectionState.isRevealOperation. - The navigation pane scrolls as an
externalrequest (alignment fromgetNavAlign('external'),autoby default). - The list pane queues a
revealrequest and scrolls once the index is ready. Startup reveals center the target item; manual reveals useauto.
- The mobile drawer raises the
notebook-navigator-visibleevent when opened. - Navigation first attempts an immediate scroll for the current selection and falls back to a
mobile-visibilitypending scroll if the pane is not ready yet. - The list pane queues a
visibility-changescroll for the selected file, preserving context when the drawer becomes visible.
- When search filters remove the selected file, the list pane detects that the file path is absent from
filePathToIndex. - Unless suppressed for mobile shortcuts, the hook queues a
topscroll with reasonlist-structure-change. - If the selected file remains in the results, the search effect does not queue an additional scroll request.
List pane priorities live in rankListPending:
export function rankListPending(p?: { type: 'file' | 'top'; reason?: ListScrollIntent }): number {
if (!p) return -1;
if (p.type === 'top') return 0;
switch (p.reason) {
case 'list-structure-change':
return 1;
case 'visibility-change':
return 2;
case 'folder-navigation':
return 3;
case 'reveal':
return 4;
default:
return 1;
}
}setPending compares these ranks and only replaces the current request when the new one is equal or higher.
- Navigation pane:
selectioncenters on mobile and usesautoon desktop.visibilityToggle,external, andmobile-visibilityuseauto.startupdefaults tocenterandrevealmaps toautoingetNavAlign. - List pane:
folder-navigationcenters on mobile, others useauto. Startup reveals override tocenterafter execution.
- Both panes pass scroll insets to TanStack Virtual so row offsets and
scrollToIndexalign with pane chrome. - Navigation runs a post-scroll adjustment step (
ensureIndexNotCovered) that corrects bottom overlap afterscrollToIndex, retrying for a few animation frames while virtualization settles.
- Navigation visibility toggles run a
requestAnimationFramecheck after scrolling to detect secondary rebuilds and queue another pending request if needed. - List
list-structure-changescrolls run a similar frame-based check and queue a follow-up when the index changes again (only whenrevealFileOnListChangesis enabled).
scrollContainerRefCallbackstores the DOM node and updates a local state reference.ResizeObserver(or a window resize fallback) tracks the node's size.isScrollContainerReadygates all scroll execution paths, preventing TanStack Virtual scroll calls while the container (or a parent) is hidden.
- Add temporary
console.logstatements nearsetPending, pending execution effects, and version increments. Obsidian ignoresconsole.debug, so useconsole.log. - The hooks include comment tags (
NAV_SCROLL_*,SCROLL_*) marking where to instrument logs.
Scroll lands on wrong item
- Confirm
minIndexVersionis set toindexVersionRef.current + 1when a rebuild is pending. - Ensure the path resolves through
getNavigationIndexorgetSelectionIndex.
Scroll does not execute
- Verify
isScrollContainerReadyis true. - Check that the pending entry still matches the current selection.
- Confirm the required version has been reached.
Multiple scrolls conflict
- Inspect
rankListPendingdecisions to see which request displaced another. - For navigation, confirm visibility-toggle guards are not blocking unrelated intents.
- Log every
indexVersionRefincrement to correlate rebuilds with pending execution. - Log path-to-index resolution results to confirm indices match expectations.
- Log pending intent, required version, and alignment when the execution effect runs.
- Log priority comparisons inside
setPendingto see why a request was replaced or kept.