Position preservation and layout changes
After initial restoration, content and layout can shift dynamically due to asynchronous data insertions, container resizing, or sticky header height changes. ngx-magic-scroll implements an anchor-relative pixel displacement policy that preserves the active section’s pixel displacement, preventing disruptive jumps back to section headings during automatic reflows.
Different causes have different policies
Section titled “Different causes have different policies”| Cause | Target | Displacement policy | Default behavior |
|---|---|---|---|
| Initial ready render | Fragment from URL snapshot | Aligned to heading (displacement = 0) |
Restore once, instantly (unless user cancelled via interaction) |
Explicit scrollTo() |
Requested anchor | Aligned to heading (displacement = 0) |
Scroll smoothly |
| External router fragment change | Route destination anchor | Aligned to heading (displacement = 0) |
Restore instantly |
| Later ready source change | Current live visible section | Preserves saved pixel displacement (savedDisplacement) |
Correct instantly without resetting to section top |
| Window resize or measured header-height change | Current live visible section | Preserves saved pixel displacement (savedDisplacement) |
Realign instantly to maintain section pixel offset |
| Region content reflow (layout expansion/image) | Current live visible section | Preserves saved pixel displacement (savedDisplacement) |
Opt-in (preserveOnLayoutChange: true); coalesced via rAF |
The distinction between initial URL fragments, live visible sections, and intentional user navigation ensures seamless reading continuity. When reading halfway through a 1200px card, layout reflows preserve the section’s relative pixel position rather than resetting to the card header.
Live section tracking vs URL debouncing
Section titled “Live section tracking vs URL debouncing”While scrolling, the active visible section geometry and anchor-relative pixel displacement are captured in real time, whereas updates to the browser’s URL router are debounced by debounceTime (default 500ms) to avoid cluttering browser history during continuous motion. When automatic realignment or data updates occur during this debounce interval, ngx-magic-scroll preserves the real-time visible section and displacement rather than falling back to a stale URL anchor.
sequenceDiagram
autonumber
actor User
participant Viewport as Scroll Container
participant Directive as MagicScrollDirective
participant Provider as NearestAnchorProvider
participant Service as ScrollService
Note over User,Directive: User scrolls into long section
User->>Viewport: Scroll to midpoint of long card
Viewport->>Directive: onScroll event
Directive->>Provider: calculateNearestAnchor()
Provider-->>Directive: liveAnchor ('card-1')
Directive->>Directive: Capture savedDisplacement = anchorRect.top - topBoundary
Note over Viewport,Directive: Automatic layout reflow occurs
alt Data prepended / Container resize / Header resize
Viewport->>Directive: Data update / ResizeObserver trigger
Directive->>Directive: preserve() called
Directive->>Directive: Compute delta = (currentAnchorRect.top - topBoundary) - savedDisplacement
Directive->>Service: Adjust scrollable.scrollTop by delta (clamped to bounds)
Service->>Viewport: scrollable.scrollTo(targetScrollTop, instant)
Note over User,Viewport: Section displacement preserved!
end
Section-relative displacement mechanics
Section titled “Section-relative displacement mechanics”1. Displacement capture during user scrolling
Section titled “1. Displacement capture during user scrolling”As the user scrolls through the container, onScroll computes the reading boundary and measures the active section’s top offset:
topBoundary = containerTop + measuredHeaderHeight + headerOffsetsavedDisplacement = activeAnchorElement.getBoundingClientRect().top - topBoundaryFor example, when reading 400px deep into a card, savedDisplacement is -400px.
2. Delta calculation and native scroll anchoring compatibility
Section titled “2. Delta calculation and native scroll anchoring compatibility”When automatic reflow occurs (e.g. items prepended above the section, or sticky header expanded):
currentDisplacement = currentAnchorElement.getBoundingClientRect().top - topBoundarydelta = currentDisplacement - savedDisplacementtargetScrollTop = clamp(currentScrollTop + delta, 0, maxScrollTop)- Without browser scroll anchoring: If content is inserted above the section,
currentDisplacementchanges by the height of the prepended items.deltaequals the shift andscrollTopis adjusted instantly, keeping the section visually locked under the reader’s view. - With native browser scroll anchoring (
overflow-anchor: auto): If the browser has already compensatedscrollTopfor inserted content,currentDisplacementremains equal tosavedDisplacement.deltacomputes to0, inherently preventing double-compensation without browser-sniffing heuristics. - Intra-section limitation: Anchor-relative displacement preserves the active section anchor’s pixel offset relative to the reading boundary (
savedDisplacement). It does not track intra-section elements or percentage progress through a section. Consequently, when text wraps or content changes inside the active section itself, the exact paragraph or sentence being read may still move; retaining a specific paragraph across internal changes requires finer-grained child anchors.
3. Reset policy for deliberate navigation
Section titled “3. Reset policy for deliberate navigation”Explicit programmatic calls (scrollTo('card-2')) and external router fragment navigations reset savedDisplacement to 0. Intentional navigation always aligns the destination section’s heading cleanly to the configured top offset.
4. Deterministic element removal handling
Section titled “4. Deterministic element removal handling”If the active section element is removed from the DOM during a data change or layout update, preserve() gracefully skips realignment and maintains the current viewport position without arbitrary jumps.
Region content reflow with preserveOnLayoutChange
Section titled “Region content reflow with preserveOnLayoutChange”In dynamic web applications, presentation-level elements within a scroll region frequently change dimensions without any updates to the fetched data source (scrollSource). Common examples include:
- Expanding an accordion or detail disclosure panel above the reader’s active section.
- Asynchronous image loads settling to their natural height without pre-set aspect ratios.
- Conditional validation messages or interactive preview cards appearing dynamically.
When preserveOnLayoutChange: true is enabled, MagicScrollDirective attaches a region-scoped ResizeObserver to the host element. Layout shifts are coalesced per animation frame (requestAnimationFrame) to eliminate layout thrashing and feedback loops.
graph TD
A[Region Content Reflow / Image Loaded / Accordion Expanded] -->|ResizeObserver Callback| B{preserveOnLayoutChange Enabled?}
B -->|No| C[Ignore Reflow]
B -->|Yes| D{Initialized & Not Interrupted?}
D -->|No / User Scrolling| E[Suppress Realignment]
D -->|Yes| F{rAF Already Queued?}
F -->|Yes| G[Coalesce into Existing Frame]
F -->|No| H[Queue requestAnimationFrame]
H --> I[Execute preserve()]
I --> J[Calculate Delta against liveAnchor & savedDisplacement]
J --> K{Delta === 0 or Anchored by Browser?}
K -->|Yes| L[No-op: Native overflow-anchor already compensated]
K -->|No| M[Scrollable.scrollTo: Adjust scrollTop instantly]
Supported scenarios vs unsupported modifications
Section titled “Supported scenarios vs unsupported modifications”- Supported (Dimension Changes): Any internal DOM expansion or contraction that changes box dimensions (such as toggling accordions, inserting child cards, or loading images) alters the region’s layout and triggers instant, seamless realignment.
- Unsupported (Position-Only Shifts): Pure CSS coordinate changes (such as
transform: translateY(...)or CSStoptransitions) that do not alter the box geometry or layout dimensions of elements in the DOM cannot be observed byResizeObserver. - User Gesture Priority: If the reader is actively scrolling via touch gestures, wheel events, or navigation keys, automated layout realignments yield immediately to user navigation.
Header measurement defines the top offset
Section titled “Header measurement defines the top offset”The effective top offset boundary is:
measured header height + headerOffsetWith no configured or matching header, the measured height is zero and headerOffset supplies the entire offset. With a header, it adds a gap below that header. Use a headerSelector that uniquely identifies the intended element; its height is measured and observed automatically via ResizeObserver.
offset() exposes the sum, and headerHeight() exposes the measurement alone. A sticky companion panel can use the same sum for its CSS top so navigation and scroll alignment share one boundary.
A per-call topOffset replaces the entire effective offset for that action. It is not an additional gap. Viewport boundary clamping ensures scroll targets are bounded within [0, scrollHeight - clientHeight].
Choose a policy for your layout
Section titled “Choose a policy for your layout”Use restoreOnDataChange: false when data updates should let the viewport move naturally without anchor preservation. Use preserveOnResize: false when automatic realignment during resizing or header changes is unwanted. Disabling layout corrections still leaves the measured header height and effective offset available.
Initial restoration and explicit interactions have separate behavior defaults: behavior.restoration is instant, and behavior.interaction is smooth. Automatic data and layout corrections remain instant.
Provider defaults are inherited through Angular’s injector hierarchy. Region scrollOptions override those defaults, and supported per-call options override the region for one scrollTo() action.
Related documentation
Section titled “Related documentation”- Data readiness and restoration explains initial restoration, user cancellation, and the event lifecycle diagram.
- Sticky master–detail shares the measured offset with a sticky sidebar.
- Paginated lists shows data updates and optional correction disabling.
- MagicScrollOptions documents configuration defaults and visibility rules.
- MagicScrollToOptions documents per-action overrides.
