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/dynamic-heights.md.

Dynamic item heights

When rows have different heights, set [dynamicItemHeight]="true". itemHeight then becomes the estimate for rows that have not been rendered yet.

Live exampleList with variable row heightsOpen in new tab
src/app/examples/dynamic-height/dynamic-height.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  DragPreviewComponent,
  DraggableDirective,
  DropEvent,
  DroppableGroupDirective,
  reorderItems,
  VirtualSortableListComponent,
} from 'ngx-virtual-dnd';

interface Note {
  id: string;
  title: string;
  body: string;
}

const SENTENCE = 'Rows are measured with a ResizeObserver as they render. ';

@Component({
  selector: 'app-dynamic-height-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <div vdndGroup="notes">
      <ng-template #noteTpl let-note>
        <div class="row note" [vdndDraggable]="note.id">
          <span class="note-title">{{ note.title }}</span>
          <span class="note-body">{{ note.body }}</span>
        </div>
      </ng-template>

      <!-- itemHeight is only the estimate for rows that have not been measured yet -->
      <vdnd-sortable-list
        class="list"
        droppableId="notes"
        [items]="notes()"
        [itemHeight]="72"
        [dynamicItemHeight]="true"
        [containerHeight]="336"
        [itemIdFn]="noteId"
        [itemTemplate]="noteTpl"
        (drop)="onDrop($event)"
      />
    </div>

    <vdnd-drag-preview />
  `,
})
export class DynamicHeightExampleComponent {
  readonly notes = signal<Note[]>(
    Array.from({ length: 500 }, (_, i) => ({
      id: `note-${i + 1}`,
      title: `Note ${i + 1}`,
      body: SENTENCE.repeat((i % 4) + 1).trim(),
    })),
  );

  readonly noteId = (note: Note): string => note.id;

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

How it works

  • Rendered rows are measured with a ResizeObserver. If a row's content changes height later, the list updates.
  • Heights are cached by item ID in each list, so they survive reordering. An item moved into another list is measured again there, starting from that list's estimate.
  • Offsets are kept as prefix sums, so finding the row at a scroll position is a binary search (O(log n)). A measured height only invalidates the offsets after its row, and they are recomputed only as far as a lookup reaches, so measuring rows while scrolling a long list stays cheap.
  • dynamicItemHeight and itemHeight can change at runtime. The list then starts again from the estimate and measures the rendered rows anew. Turning dynamicItemHeight off stops measuring.

Two rules from Core concepts matter most here. The draggable ID must equal the item ID, and vdndDraggable must be on the template's root element. The list uses both to find the element it measures.

Choosing the estimate

Pick an estimate close to your typical row height. A poor estimate still works, but the scrollbar size and position shift more as rows are measured.

Other components

The same input exists on every virtual container:

<vdnd-virtual-scroll [itemHeight]="80" [dynamicItemHeight]="true" ... />

<vdnd-virtual-content [itemHeight]="80" [dynamicItemHeight]="true" vdndDroppable="notes">
  ...
</vdnd-virtual-content>

<!-- *vdndVirtualFor directly inside a vdndScrollable, with no parent viewport -->
<ng-container
  *vdndVirtualFor="let note of notes(); itemHeight: 80; dynamicItemHeight: true; trackBy: trackById"
>
  ...
</ng-container>

Inside <vdnd-virtual-content> or <vdnd-virtual-viewport>, *vdndVirtualFor inherits itemHeight and dynamicItemHeight from its parent. Directly inside a vdndScrollable, it positions each row absolutely at its measured offset (see VirtualForDirective).