Bug Description
In reading mode, a note carrying the in-note task card jumps under the scroll, repeatedly and jarringly enough to make reading mode unusable. Only notes carrying the card are affected; notes without it are steady, independent of length or content.
The cause is a re-injection cycle rather than anything about the widget's size or position.
TaskNotes injects the card as a direct child of .markdown-preview-sizer (insertAfterMetadataOrHeader, TaskCardNoteDecorations.ts). Obsidian's reading view is virtualised, and every updateVirtualDisplay ends with:
sizerEl.setChildrenInPlace([pusherEl, ...shownSections])
setChildrenInPlace removes every child not in that list, so Obsidian deletes the card on every render pass. Since 4.12.0, src/editor/ReadingModeWidgetObserver.ts re-injects it on the next animation frame. During a scroll that cycle runs continuously, and updateVirtualDisplay finishes by re-asserting previewEl.scrollTop each pass. That re-assertion is what is seen as the note jumping.
Isolated by a single-variable test on one note (125 sections), driving an identical scroll of 14 steps of 220px each time and counting childList mutations on the sizer:
| Condition |
Jumps? |
Sizer mutations |
Scroll corrections |
showTaskCardInNote: false |
No |
0 |
0 |
| Card on, as shipped |
Yes |
110 |
4, about 16px each |
| Card on, forced to zero height by CSS |
Yes |
- |
- |
The third row is the one that isolates it. With height: 0; overflow: hidden and margins, padding and border zeroed, the card contributes exactly as much layout as it does when the setting is off, and it still jumps. So this is not a sizing or reflow problem - it is the DOM churn.
Two things that look like the cause and are not
Offered because both are natural first guesses and cost me an afternoon.
Section height accounting. measureSection computes a section's height as el.nextSibling.offsetTop - el.offsetTop. The card sits between the header section and the next one, so its height is counted in neither, and the model runs short. This is real, but far too small to be the symptom: total model error peaks at about 26px roughly three seconds after a note opens and settles to 4px, while a non-task control note measures 0px across 1,100+ samples. The jumping continues while the model is within a few pixels of correct.
Layout shift. There are no layout-shift performance entries at all during the jump. The content is not moving; the scroll position is being re-asserted against the renderer's own model.
The obvious fix does not work, and here is the measurement
Nesting the widget inside .mod-header.mod-ui looks like the answer, and half of it is:
setChildrenInPlace only manages direct children of the sizer, so a nested widget is never deleted by it.
- The re-injection guard is
sizer.querySelector(widgetSelector), and querySelector is recursive, so a nested widget still satisfies it and no re-injection is scheduled.
- Prototyped live: the card renders correctly at 63px in its new home, and a single move places it.
But .mod-header.mod-ui is itself sections[0], and Obsidian detaches sections that scroll out of the render window. Scrolling past the top of the note removes the header and the nested widget with it; the guard then finds no widget anywhere in the sizer and re-injects as a direct child, restarting the cycle. Measured on the same scroll: 87 sizer mutations, 6 widget add/remove events, and 2 scroll corrections of 79px - worse per event, because the widget's height now perturbs the header section's measurement as well. The add and the remove land in the same microtask batch, so a MutationObserver cannot intervene between them.
So the widget needs a host that is neither a direct child of the sizer nor inside a virtualised section. Within the sizer only pusherEl survives every pass, since it is always in the keep-list, but it is a 1x0.1px box.
Suggested directions
Offered as starting points, not prescriptions:
- Do not re-inject while a scroll is in flight. The cycle only hurts because it runs per-frame during scrolling; coalescing re-injection until the scroll settles would remove the symptom without moving the widget.
- Distinguish "removed because offscreen" from "removed because the DOM was rebuilt." The observer currently treats any absence as a reason to re-inject. Obsidian's own virtualisation is the commonest cause of absence, and re-injecting against it is what creates the loop.
- Render outside
.markdown-preview-sizer. Nothing outside it is virtualised, though the widget would then be pinned rather than scrolling with the note, and the guard would need changing to match.
Relationship to #2081
This is the same underlying issue as #2081 wearing a different face, and not a complaint about that fix. Before 4.12.0 the widget was deleted by setChildrenInPlace and simply stayed gone, which is #2081's reported symptom. ReadingModeWidgetObserver made it come back, which is correct as far as it goes, but the deletion still happens on every pass, so the widget now churns instead of vanishing. Fixing the deletion rather than the absence would close both.
#1436 ("Error injecting task card widget in reading mode") and #513 ("Subtasks are not showing in Preview Mode") both look like the same root cause, from the pre-4.12.0 side.
Steps to Reproduce
- Confirm
showTaskCardInNote: true.
- Open a note tagged
#task that is long enough to scroll, in reading mode.
- Scroll down through it. The content jumps repeatedly.
- Open a note of similar length without
#task, in reading mode, and scroll. It is steady.
- Turn Show task card in note off and repeat step 3. The jumping stops.
- To rule out size as the variable, turn it back on and add a snippet:
.tasknotes-task-card-note-widget { height: 0; min-height: 0; margin-block: 0; padding: 0; border: 0; overflow: hidden; }. The card now contributes no layout at all, and step 3 still jumps.
Live Preview is unaffected throughout, on every platform tested.
TaskNotes Debug Info
SYSTEM INFO:
Obsidian version: 1.13.7
Installer version: 1.13.4
Operating system: Darwin Kernel Version 25.6.0: Sat Jul 11 15:26:29 PDT 2026; root:xnu-12377.161.13~4/RELEASE_ARM64_T8132 25.6.0
Login status: logged in
Language: en-GB
Catalyst license: none
Insider build toggle: off
Live preview: on
Base theme: adapt to system
Community theme: Obsidian gruvbox 0.2.1
Snippets enabled: 3
Restricted mode: off
Plugins installed: 20
Plugins enabled: 16
1: Image Converter v1.4.6
2: Calendar v1.5.10
3: Dataview v0.5.68
4: Templater v2.25.0
5: Natural Language Dates v0.6.4
6: Metadata Menu v0.8.12
7: Mermaid Tools v1.4.1
8: Excalidraw v2.26.4
9: Local REST API with MCP v5.1.0
10: Note Toolbar v1.34.13
11: Local Images Plus v0.16.4
12: Another Quick Switcher v14.7.0
13: Highlight active folder section v1.5.0
14: SD Vault Tools v4.118.1
15: JO Vault Tools v1.34.0
16: TaskNotes v4.12.4
RECOMMENDATIONS:
Custom theme and snippets: for cosmetic issues, please first try updating your theme and disabling your snippets. If still not fixed, please try to make the issue happen in the Sandbox Vault or disable community theme and snippets.
Community plugins: for bugs, please first try updating all your plugins to latest. If still not fixed, please try to make the issue happen in the Sandbox Vault or disable community plugins.
Affected Task Frontmatter
Relevant Bases File Content
Relevant Settings or Customizations
No response
Console Errors or Logs
Screenshots or Screen Recording
No response
Bug Description
In reading mode, a note carrying the in-note task card jumps under the scroll, repeatedly and jarringly enough to make reading mode unusable. Only notes carrying the card are affected; notes without it are steady, independent of length or content.
The cause is a re-injection cycle rather than anything about the widget's size or position.
TaskNotes injects the card as a direct child of
.markdown-preview-sizer(insertAfterMetadataOrHeader,TaskCardNoteDecorations.ts). Obsidian's reading view is virtualised, and everyupdateVirtualDisplayends with:setChildrenInPlaceremoves every child not in that list, so Obsidian deletes the card on every render pass. Since 4.12.0,src/editor/ReadingModeWidgetObserver.tsre-injects it on the next animation frame. During a scroll that cycle runs continuously, andupdateVirtualDisplayfinishes by re-assertingpreviewEl.scrollTopeach pass. That re-assertion is what is seen as the note jumping.Isolated by a single-variable test on one note (125 sections), driving an identical scroll of 14 steps of 220px each time and counting
childListmutations on the sizer:showTaskCardInNote: falseThe third row is the one that isolates it. With
height: 0; overflow: hiddenand margins, padding and border zeroed, the card contributes exactly as much layout as it does when the setting is off, and it still jumps. So this is not a sizing or reflow problem - it is the DOM churn.Two things that look like the cause and are not
Offered because both are natural first guesses and cost me an afternoon.
Section height accounting.
measureSectioncomputes a section's height asel.nextSibling.offsetTop - el.offsetTop. The card sits between the header section and the next one, so its height is counted in neither, and the model runs short. This is real, but far too small to be the symptom: total model error peaks at about 26px roughly three seconds after a note opens and settles to 4px, while a non-task control note measures 0px across 1,100+ samples. The jumping continues while the model is within a few pixels of correct.Layout shift. There are no
layout-shiftperformance entries at all during the jump. The content is not moving; the scroll position is being re-asserted against the renderer's own model.The obvious fix does not work, and here is the measurement
Nesting the widget inside
.mod-header.mod-uilooks like the answer, and half of it is:setChildrenInPlaceonly manages direct children of the sizer, so a nested widget is never deleted by it.sizer.querySelector(widgetSelector), andquerySelectoris recursive, so a nested widget still satisfies it and no re-injection is scheduled.But
.mod-header.mod-uiis itselfsections[0], and Obsidian detaches sections that scroll out of the render window. Scrolling past the top of the note removes the header and the nested widget with it; the guard then finds no widget anywhere in the sizer and re-injects as a direct child, restarting the cycle. Measured on the same scroll: 87 sizer mutations, 6 widget add/remove events, and 2 scroll corrections of 79px - worse per event, because the widget's height now perturbs the header section's measurement as well. The add and the remove land in the same microtask batch, so aMutationObservercannot intervene between them.So the widget needs a host that is neither a direct child of the sizer nor inside a virtualised section. Within the sizer only
pusherElsurvives every pass, since it is always in the keep-list, but it is a 1x0.1px box.Suggested directions
Offered as starting points, not prescriptions:
.markdown-preview-sizer. Nothing outside it is virtualised, though the widget would then be pinned rather than scrolling with the note, and the guard would need changing to match.Relationship to #2081
This is the same underlying issue as #2081 wearing a different face, and not a complaint about that fix. Before 4.12.0 the widget was deleted by
setChildrenInPlaceand simply stayed gone, which is #2081's reported symptom.ReadingModeWidgetObservermade it come back, which is correct as far as it goes, but the deletion still happens on every pass, so the widget now churns instead of vanishing. Fixing the deletion rather than the absence would close both.#1436 ("Error injecting task card widget in reading mode") and #513 ("Subtasks are not showing in Preview Mode") both look like the same root cause, from the pre-4.12.0 side.
Steps to Reproduce
showTaskCardInNote: true.#taskthat is long enough to scroll, in reading mode.#task, in reading mode, and scroll. It is steady..tasknotes-task-card-note-widget { height: 0; min-height: 0; margin-block: 0; padding: 0; border: 0; overflow: hidden; }. The card now contributes no layout at all, and step 3 still jumps.Live Preview is unaffected throughout, on every platform tested.
TaskNotes Debug Info
Affected Task Frontmatter
Relevant Bases File Content
Relevant Settings or Customizations
No response
Console Errors or Logs
Screenshots or Screen Recording
No response