For AI agents: the complete documentation index is available at https://gultyayev.github.io/ngx-virtual-dnd/llms.txt, the full documentation bundle is available at https://gultyayev.github.io/ngx-virtual-dnd/llms-full.txt, and this page is available as Markdown at https://gultyayev.github.io/ngx-virtual-dnd/guide/features/page-scroll.md.

Page-level scrolling

Sometimes the list is not its own scroll box: it scrolls together with a page header, other content and a footer. Mark the element that actually scrolls with vdndScrollable, and render items with <vdnd-virtual-content> and *vdndVirtualFor.

Live exampleList that scrolls with its header and footerOpen in new tab
src/app/examples/page-scroll/page-scroll.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  ContentHeaderDirective,
  DraggableDirective,
  DragPreviewComponent,
  DropEvent,
  DroppableDirective,
  DroppableGroupDirective,
  reorderItems,
  ScrollableDirective,
  VirtualContentComponent,
  VirtualForDirective,
} from 'ngx-virtual-dnd';

interface Task {
  id: string;
  title: string;
}

@Component({
  selector: 'app-page-scroll-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    ScrollableDirective,
    VirtualContentComponent,
    VirtualForDirective,
    ContentHeaderDirective,
    DraggableDirective,
    DroppableDirective,
    DroppableGroupDirective,
    DragPreviewComponent,
  ],
  template: `
    <!-- The element that scrolls: it needs a height and overflow: auto -->
    <div class="page" vdndScrollable>
      <div vdndGroup="today">
        <vdnd-virtual-content [itemHeight]="48" vdndDroppable="today" (drop)="onDrop($event)">
          <!-- Measured automatically, and scrolls together with the rows -->
          <h3 class="page-header" vdndContentHeader>Today</h3>

          <ng-container *vdndVirtualFor="let task of tasks(); trackBy: trackById">
            <div class="row" [vdndDraggable]="task.id">{{ task.title }}</div>
          </ng-container>
        </vdnd-virtual-content>
      </div>

      <footer class="page-footer">Regular content after the list</footer>
    </div>

    <vdnd-drag-preview />
  `,
})
export class PageScrollExampleComponent {
  readonly tasks = signal<Task[]>(
    Array.from({ length: 500 }, (_, i) => ({ id: `task-${i + 1}`, title: `Task ${i + 1}` })),
  );

  readonly trackById = (_index: number, task: Task): string => task.id;

  onDrop(event: DropEvent): void {
    reorderItems(event, this.tasks);
  }
}

The element marked vdndScrollable must be the one that scrolls, so give it a height and an overflow:

.page {
  height: 100vh; /* the example uses 400px */
  overflow: auto;
}
  • <vdnd-virtual-content> must be inside the vdndScrollable element.
  • Inside <vdnd-virtual-content>, *vdndVirtualFor inherits itemHeight and dynamicItemHeight from it, and the droppable ID from the enclosing vdndDroppable. Only trackBy is required.
  • vdndContentHeader marks a header inside the component. Its height is measured and used as the list's top offset. If the header lives outside the component, pass its height with [contentOffset] instead.
  • Auto-scroll uses the vdndScrollable element, so dragging near the top or bottom of the page scrolls it.

Sticky headers and footers

A header or footer with position: sticky inside the scroll container stays on screen while the rows scroll behind it. The drag can't tell it covers the container, so tell it with scrollInsetTop and scrollInsetBottom: the height in pixels each covers. Measure them however suits your layout:

<div
  vdndScrollable
  [scrollInsetTop]="headerHeight()"
  [scrollInsetBottom]="footerHeight()"
>
  <header class="sticky-header">...</header>
  <vdnd-virtual-content [itemHeight]="72" [contentOffset]="headerHeight()" vdndDroppable="tasks">
    ...
  </vdnd-virtual-content>
  <footer class="sticky-footer">...</footer>
</div>
.sticky-header {
  position: sticky;
  top: 0;
}
.sticky-footer {
  position: sticky;
  bottom: 0;
}

The drag then treats the part of the container between them as the container:

  • With constrainToContainer, the preview stays between the header and the footer.
  • Auto-scroll starts at the header's lower edge and the footer's upper edge. A pointer over either scrolls at full speed.
  • The pointer over the header or footer is not over the list, as outside the container. The drop position still follows the preview's top edge, as at any edge of the container, so without constrainToContainer a preview pulled over the header can point at a row behind it.
  • A keyboard drag scrolls the list to keep the placeholder between the header and the footer.
  • A list in a scroll container nested inside this one is treated the same way where it scrolls under the header or footer: it is not targeted there, auto-scroll over them scrolls the outer container, and its constrained preview and keyboard placeholder stay clear of them.

When the header collapses or grows while the page scrolls, keep the height you pass up to date: a drag in progress measures again when it changes, and a keyboard drag scrolls its placeholder clear of them again.

Measure the insets from the container's outer (border) edges: a border on the container adds its width, and a horizontal scrollbar under a bottom: 0 footer adds its height.

<vdnd-sortable-list>, <vdnd-virtual-scroll> and <vdnd-virtual-viewport> take the same inputs for content overlaid on their own rows, such as a header positioned over the top of the list. They also leave that space around their rows: the first row starts below the top inset, and the list scrolls far enough for the last row to clear the bottom one. In <vdnd-virtual-viewport>, the rows start contentOffset down when that is more than the top inset.

Ionic

<ion-content> scrolls an element inside its shadow DOM, so vdndScrollable cannot go on ion-content itself. Turn off its scrolling and add a scroll host in the light DOM:

<ion-content [scrollY]="false">
  <div class="ion-content-scroll-host" vdndScrollable>
    <!-- vdndGroup, vdnd-virtual-content, footer... -->
  </div>
</ion-content>
.ion-content-scroll-host {
  height: 100%;
  overflow-y: auto;
}

The page scroll demo is a complete Ionic task manager built this way.