--- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/start/introduction.md --- > 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. # Introduction ngx-virtual-dnd is a drag-and-drop library for Angular, built for **virtually scrolled lists**. It renders only the rows in view plus a small buffer, so the DOM stays small however long the list grows. Drag and drop still works across the whole list. Live exampleSortable list with 1,000 items[Open in new tab](/ngx-virtual-dnd/demo/examples/quick-start/) Try it: drag a row, or focus one with Tab , press Space and move it with the arrow keys. The list holds 1,000 tasks, but only about a dozen rows are in the DOM at any time. ## Why a separate library Most drag-and-drop libraries work out the drop position by measuring sibling elements in the DOM. With virtual scrolling most siblings do not exist, so those libraries drop items in the wrong place, or cannot scroll to a target that has not been rendered yet. ngx-virtual-dnd works from geometry instead. At drag start it records the position and size of each drop target. While you drag, it compares the pointer with those recorded bounds and calculates the drop index from the scroll offset and row heights. See [How it works](https://gultyayev.github.io/ngx-virtual-dnd/guide/more/how-it-works.md) for details. The approach was inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtualized-dnd). ## Features - **Virtual scrolling** with fixed or [dynamic item heights](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/dynamic-heights.md) - **[Multiple lists](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/multiple-lists.md)** that exchange items within a group - **[Page-level scrolling](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/page-scroll.md)**, including Ionic `ion-content` - **[Auto-scroll](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/auto-scroll.md)** near container edges, with configurable speed and threshold - **[Drag handles, delays, axis locking and constraints](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md)** - **[Custom previews and placeholders](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/preview-and-placeholder.md)** - **[Shift animations and haptics](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/animations.md)**, opt-in - **[Keyboard and screen reader support](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/accessibility.md)** - **Angular 21+:** standalone components, signals, `OnPush`-friendly ## When to use it Use ngx-virtual-dnd for **vertical lists**, such as sortable lists and kanban-style boards, especially when a list can grow to hundreds or thousands of items. It is not a general-purpose drag-and-drop toolkit. Freeform canvases, grids with 2D reordering and dragging files from the desktop are out of scope. See [Known limitations](https://gultyayev.github.io/ngx-virtual-dnd/guide/more/limitations.md). ## Next steps - [Install the package](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/installation.md) - [Build a sortable list](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/quick-start.md) in a few minutes - Moving from `@angular/cdk/drag-drop`? See [Coming from Angular CDK](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/coming-from-cdk.md) - Using an AI coding assistant? [Install the agent skill](https://gultyayev.github.io/ngx-virtual-dnd/guide/more/ai-assistants.md) --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/start/installation.md --- > 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. # Installation ```sh [npm] npm install ngx-virtual-dnd ``` ```sh [yarn] yarn add ngx-virtual-dnd ``` ```sh [pnpm] pnpm add ngx-virtual-dnd ``` ```sh [bun] bun add ngx-virtual-dnd ``` ```sh [deno] deno add npm:ngx-virtual-dnd ``` ## Requirements | Requirement | Version | | ----------------- | --------------------------------------- | | `@angular/core` | `^21.0.0 \|\| ^22.0.0` | | `@angular/common` | `^21.0.0 \|\| ^22.0.0` | | Browsers | Latest Chrome, Edge, Firefox and Safari | Its only runtime dependency is `tslib`. ## Imports Every component, directive, utility and type is exported from the package root. They are all standalone, so you import them directly in the component that uses them: ```ts import { DraggableDirective, DragPreviewComponent, DropEvent, DroppableGroupDirective, moveItem, VirtualSortableListComponent, } from 'ngx-virtual-dnd'; ``` There are no providers to register and no global stylesheet to include. The one optional token, `VDND_ANIMATION_CONFIG`, turns on [shift and drop animations](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/animations.md). The library only sets the layout styles that virtual scrolling needs. How items, lists and the drop placeholder look is up to your app. See [Styling](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/styling.md) for the CSS classes the library adds. ## Next Build your first list in the [Quick start](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/quick-start.md). --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/start/quick-start.md --- > 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. # Quick start This page builds a sortable list of 1,000 tasks. `` combines the drop target, the virtual scroller and the drop placeholder, so it is the right starting point for most apps. Live exampleSortable list with 1,000 items[Open in new tab](/ngx-virtual-dnd/demo/examples/quick-start/) ## The code ```ts file="/src/app/examples/quick-start/quick-start.ts" import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { DragPreviewComponent, DraggableDirective, DropEvent, DroppableGroupDirective, reorderItems, VirtualSortableListComponent, } from 'ngx-virtual-dnd'; interface Task { id: string; title: string; } @Component({ selector: 'app-quick-start-example', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ VirtualSortableListComponent, DroppableGroupDirective, DraggableDirective, DragPreviewComponent, ], template: `
{{ task.title }}
`, }) export class QuickStartExampleComponent { // 1,000 items; only the visible rows are in the DOM. readonly tasks = signal( Array.from({ length: 1000 }, (_, i) => ({ id: `task-${i + 1}`, title: `Task ${i + 1}` })), ); readonly taskId = (task: Task): string => task.id; onDrop(event: DropEvent): void { reorderItems(event, this.tasks); } } ``` ## What each part does 1. **`vdndGroup="tasks"`** names the group that draggables and drop targets belong to. Items can only move between drop targets in the same group. A group is required even for a single list; without one, dragging is disabled. 2. **The item template** is declared _inside_ the `vdndGroup` element. Angular templates resolve dependency injection where they are declared, so this is how each draggable finds its group. 3. **`[vdndDraggable]="task.id"`** makes each row draggable. The ID must equal what `itemIdFn` returns for that item, and it must sit on the template's root element. 4. **``** renders the visible rows. `itemHeight` is the row height in pixels and `containerHeight` is the height of the scroll area. 5. **`(drop)`** fires when an item is dropped. The library never changes your data. `reorderItems()` applies the move to the `tasks` signal. 6. **``** renders the element that follows the pointer. Place it once, anywhere in the template. :::tip Keyboard support is built in Every draggable can be focused with Tab . Space picks it up, the arrow keys move it, and Space or Enter drops it. See [Accessibility](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/accessibility.md) . ::: ## Styles The library does not style rows or the drop placeholder, so the code above renders plain text rows and an invisible placeholder. Add at least this CSS: ```css /* In the component's styles: the row template is declared in its template */ .row { display: flex; align-items: center; box-sizing: border-box; height: 48px; /* exactly itemHeight */ padding: 0 16px; border-bottom: 1px solid #e2e7f0; background: #fff; } /* In a global stylesheet (styles.css): the library renders the placeholder */ .vdnd-drag-placeholder-visible { box-sizing: border-box; border: 1.5px dashed #0e9488; background: #e4f3f2; } ``` :::warning Rows must be exactly itemHeight tall With fixed heights, every row must be exactly `itemHeight` pixels tall, borders and padding included. Otherwise the rows on screen drift away from the positions the list calculates, and items drop in the wrong place. ::: For rows that differ in height, see [Dynamic item heights](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/dynamic-heights.md). [Styling](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/styling.md) lists every class the library adds. ## Next steps - [Core concepts](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.md): what groups, draggables and droppables are, and the rules that keep them working - [Multiple lists](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/multiple-lists.md): move items between lists - [Choosing a component](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/choosing-a-component.md): when to use something other than `` --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/start/coming-from-cdk.md --- > 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. # Coming from Angular CDK The Angular CDK's drag and drop (`@angular/cdk/drag-drop`) finds drop positions by measuring the rendered siblings, so it does not support virtual scrolling: in a `cdk-virtual-scroll-viewport`, most siblings are not rendered. ngx-virtual-dnd covers the same sortable-list use cases with virtual scrolling. This page maps one API to the other. ## A sortable list, before and after With the CDK: ```angular-html
@for (task of tasks; track task.id) {
{{ task.title }}
}
``` ```ts drop(event: CdkDragDrop): void { moveItemInArray(this.tasks, event.previousIndex, event.currentIndex); } ``` With ngx-virtual-dnd: ```angular-html
{{ task.title }}
``` ```ts onDrop(event: DropEvent): void { reorderItems(event, this.tasks); // tasks is a WritableSignal } ``` The differences that matter: - **The list renders the rows.** You pass the items and a row template instead of writing the `@for` loop, because only the visible rows are rendered. - **Every list needs a group**, even a single one. A group replaces `cdkDropListConnectedTo` and `cdkDropListGroup`: lists in the same group exchange items. - **Items and lists have string IDs.** The draggable's ID must equal what `itemIdFn` returns for its item. - **The preview is rendered once**, by ``, not per item. - **Rows have a known height.** Pass it as `itemHeight`, or turn on [`dynamicItemHeight`](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/dynamic-heights.md) to measure them. - **Your data is never mutated.** The helpers update signals or return new arrays. [Core concepts](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.md) explains each of these. ## API mapping ### Lists and items | Angular CDK | ngx-virtual-dnd | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cdkDropList` with an `@for` of `cdkDrag` items | ``, or [another list component](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/choosing-a-component.md) | | `cdkDrag` | `[vdndDraggable]="item.id"` | | `cdkDropListConnectedTo`, `cdkDropListGroup` | `vdndGroup="name"` around the lists | | `cdkDragData` | `[vdndDraggableData]` | | `cdkDropListData` | `droppableData` (`vdndDroppableData` on `vdndDroppable`). Lists are usually told apart by `droppableId` | | `cdkDragHandle` | `dragHandle=""` on the draggable. See [Drag handles](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md#drag-handles) | | `cdkDragDisabled`, `cdkDropListDisabled` | `disabled` | | `cdkDragStartDelay` | `dragDelay` | | `dragStartThreshold` (`CDK_DRAG_CONFIG`) | `dragThreshold` | | `cdkDragLockAxis` | `lockAxis`, but **inverted**: it names the frozen axis. See [Axis locking](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md#axis-locking) | | `cdkDragBoundary` | `constrainToContainer` on the list. See [Constraining to the container](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md#constraining-to-the-container) | | `*cdkDragPreview` | ``. See [Custom preview](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/preview-and-placeholder.md#custom-preview) | | `*cdkDragPlaceholder` | No template. Style the `.vdnd-drag-placeholder` class | | `cdkDropListEnterPredicate` | Bind `disabled` from the drag state. See [Accepting only some items](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/multiple-lists.md#accepting-only-some-items) | | `cdkDropListOrientation` | Lists are vertical; `horizontal` and `mixed` are not supported | | `cdkDropListSortingDisabled` | Not supported | ### Events | Angular CDK | ngx-virtual-dnd | | --------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `(cdkDropListDropped)` | `(drop)`, on the destination list only | | `(cdkDragStarted)`, `(cdkDragEnded)` | `(dragStart)`, `(dragEnd)` on the draggable | | `(cdkDropListSorted)` | `(placeholderMove)` | | `(cdkDropListEntered)`, `(cdkDropListExited)` | No output. Read `activeDroppableId()` from `DragStateService`, or style `.vdnd-droppable-active` | `CdkDragDrop` maps to `DropEvent` like this: | `CdkDragDrop` | `DropEvent` | | ------------------- | ------------------------- | | `previousIndex` | `source.index` | | `currentIndex` | `destination.index` | | `previousContainer` | `source.droppableId` | | `container` | `destination.droppableId` | | `item.data` | `source.data` | | `container.data` | `destination.data` | The indexes mean the same as in the CDK: remove the item at `source.index`, then insert it at `destination.index`. Releasing outside every list emits no `(drop)` at all. `(dragEnd)` reports it with `destinationIndex: null`. ### Helpers | Angular CDK | ngx-virtual-dnd | | ------------------------------------------------- | ------------------------------------------------------- | | `moveItemInArray(array, from, to)` | `reorderItems(event, listSignal)` | | `transferArrayItem(from, to, fromIndex, toIndex)` | `moveItem(event, { todo: this.todo, done: this.done })` | | Both, for immutable state | `applyMove(event, lists)` returns new arrays | `moveItem()` handles moves within one list and between lists, so one handler serves every list. See [Handling drops](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/handling-drops.md). ### CSS classes | Angular CDK | ngx-virtual-dnd | | ------------------------- | ----------------------------------------------------------------------------------- | | `.cdk-drag-preview` | `.vdnd-drag-preview` | | `.cdk-drag-placeholder` | `.vdnd-drag-placeholder` | | `.cdk-drag-animating` | `.vdnd-drag-preview-dropping`, while the preview glides into place | | `.cdk-drop-list-dragging` | `.vdnd-droppable-active` on the targeted list, `body.vdnd-dragging` during any drag | | `.cdk-drag-disabled` | `.vdnd-draggable-disabled` | | `.cdk-drop-list-disabled` | `.vdnd-droppable-disabled` | CDK apps usually animate rows with a CSS `transition` on `.cdk-drag`. Remove it when you migrate: here, animations are opt-in through [`VDND_ANIMATION_CONFIG`](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/animations.md), and a transition on rows competes with them. --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.md --- > 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. # Core concepts Every setup, from one list to a full board, is made of the same four parts. | Concept | What it is | Declared with | | ------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | **Droppable** | A list that items can be dropped into, identified by an ID. | `vdndDroppable="id"`, or `droppableId` on `` | | **Draggable** | An item that can be picked up, identified by an ID. | `[vdndDraggable]="item.id"` | | **Group** | A name linking draggables and droppables. Items move only between droppables of the same group. | `vdndGroup="name"` on an ancestor element | | **Preview** | The element that follows the pointer during a drag. | ``, placed once | ## Life of a drag 1. The user presses on a draggable and moves past the drag threshold (5 px by default), or presses Space on a focused draggable. 2. The dragged element is hidden with `display: none`. The preview appears under the pointer and an empty **placeholder** marks the drop position in the target list. 3. As the pointer moves, the library works out which droppable is under it and at which index the item would land. Near the edges of a scrollable list it scrolls automatically. 4. On release, the dragged draggable emits `(dragEnd)`, whether the item was dropped or not. If it was dropped, the **destination** droppable then emits `(drop)` with the source and destination indexes. Both fire before the next render. 5. Your handler updates the data. The library never changes your arrays. ```ts onDrop(event: DropEvent): void { // Same list or across lists, keyed by droppable ID moveItem(event, { todo: this.todo, done: this.done }); } ``` See [Handling drops](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/handling-drops.md) for the event shape and the helper functions. ## Rules that fail silently Breaking any of these rules causes no build error. The list just does not drag, or drops items in the wrong place. 1. **Every draggable and droppable needs a group**, even when there is only one list. Wrap them in `vdndGroup`, or set `vdndDraggableGroup` / `vdndDroppableGroup` (`group` on ``). Without a group, dragging is disabled and the only hint is a warning in the console in dev mode. 2. **Declare the item `` inside the `vdndGroup` element.** Templates resolve injection where they are declared, not where they are rendered. 3. **The draggable ID must equal the list's item ID**: what `itemIdFn(item)` returns, or the `trackBy` key for `*vdndVirtualFor`. IDs are strings, so convert numeric IDs the same way in both places. 4. **Draggable IDs must be unique across all lists**, and droppable IDs must be unique on the page. 5. **Render `` once.** Without it, dragging works but nothing follows the pointer. 6. **Give every virtual list a height**, with `containerHeight` or with CSS. A list without a height collapses to 0 px. To size `` with CSS, set the height on its inner `vdnd-virtual-scroll` from a global stylesheet. 7. **Put `vdndDraggable` on the root element of the item template.** A wrapper around it would stay in the list during the drag and leave a gap. 8. **Bind `(drop)` on every list that can receive items.** It fires on the destination only. 9. **With fixed heights, make every row exactly `itemHeight` pixels tall**, borders and padding included. Otherwise the rows on screen drift away from the positions the list calculates. For rows of different heights, use [`dynamicItemHeight`](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/dynamic-heights.md). [Troubleshooting](https://gultyayev.github.io/ngx-virtual-dnd/guide/more/troubleshooting.md) maps common symptoms back to these rules. --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/handling-drops.md --- > 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. # Handling drops The library never mutates your data. When an item is dropped, the destination droppable emits a `DropEvent`, and you apply the move. ## The drop event ```ts interface DropEvent { source: { draggableId: string; droppableId: string; index: number; // where the item was data?: unknown; // vdndDraggableData }; destination: { droppableId: string; placeholderId: string; // deprecated: always END_OF_LIST; use index for the position index: number; // where the item goes, after it is removed from the source data?: unknown; // vdndDroppableData (droppableData on vdnd-sortable-list) }; } ``` `destination.index` is the insertion index **after** the item is removed from its source. "Remove at `source.index`, then insert at `destination.index`" is always correct, whether the move stays in one list or crosses lists. With virtual scrolling most items are not in the DOM, so always work from the event's indexes, never from DOM order. ## Helpers | Function | Use when | | ----------------------------- | ------------------------------------------------------------------------------ | | `moveItem(event, lists)` | Your lists are `WritableSignal`. Works within one list and across lists. | | `reorderItems(event, list)` | A single signal-based list. Reorders only. | | `applyMove(event, lists)` | Immutable state (a store, a service). Takes plain arrays and returns new ones. | | `isNoOpDrop(event)` | `true` when the item was dropped back where it started. | | `insertAt(list, item, index)` | Returns a new array with the item inserted. | | `removeAt(list, index)` | Returns a new array without the item at `index`. | ### Signals ```ts onDrop(event: DropEvent): void { moveItem(event, { todo: this.todo, done: this.done }); } ``` ### Stores and immutable state ```ts onDrop(event: DropEvent): void { if (isNoOpDrop(event)) return; const lists = applyMove(event, { todo: this.store.todo(), done: this.store.done(), }); this.store.setLists(lists); } ``` `applyMove()` returns a shallow copy of the record, with new arrays for the lists that changed. ### Saving to a server Apply the move straight away and roll back if the request fails: ```ts onDrop(event: DropEvent): void { if (isNoOpDrop(event)) return; const previous = { todo: this.todo(), done: this.done() }; moveItem(event, { todo: this.todo, done: this.done }); this.api .moveTask(event.source.draggableId, event.destination.droppableId, event.destination.index) .subscribe({ error: () => { this.todo.set(previous.todo); this.done.set(previous.done); }, }); } ``` ## Drag lifecycle events Draggables also emit `(dragStart)` and `(dragEnd)`: ```angular-html
{{ task.title }}
``` `(dragEnd)` fires after every drag, dropped or not. Check `event.destinationIndex === null` to detect "no drop": the drag was cancelled (for example with Escape ), or released outside every droppable or over a disabled one. Don't rely on `cancelled` , which is `true` only in the first case. [`DragEndEvent`](https://gultyayev.github.io/ngx-virtual-dnd/api/events.md#dragendevent) lists every way a drag can end without a drop. During a drag, droppables and `` emit `(placeholderMove)` each time the placeholder moves within them. Use it for per-step feedback such as haptics. See [Animations and haptics](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/animations.md#placeholder-move-events). To react to drags anywhere in the app, for example to show a hint, inject `DragStateService` and read its signals, such as `isDragging()` and `activeDroppableId()`. See the [API reference](https://gultyayev.github.io/ngx-virtual-dnd/api/services.md). --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/multiple-lists.md --- > 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. # Multiple lists Lists that share a **group** exchange items. Drag a row from one column to the other: Live exampleTwo lists sharing a group[Open in new tab](/ngx-virtual-dnd/demo/examples/multiple-lists/) ```ts file="/src/app/examples/multiple-lists/multiple-lists.ts" import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { DragPreviewComponent, DraggableDirective, DropEvent, DroppableGroupDirective, moveItem, VirtualSortableListComponent, } from 'ngx-virtual-dnd'; interface Task { id: string; title: string; } const createTasks = (prefix: string, count: number): Task[] => Array.from({ length: count }, (_, i) => ({ id: `${prefix}-${i + 1}`, title: `${prefix === 'todo' ? 'Task' : 'Done'} ${i + 1}`, })); @Component({ selector: 'app-multiple-lists-example', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ VirtualSortableListComponent, DroppableGroupDirective, DraggableDirective, DragPreviewComponent, ], template: `
{{ task.title }}

To do {{ todo().length }}

Done {{ done().length }}

`, }) export class MultipleListsExampleComponent { readonly todo = signal(createTasks('todo', 200)); readonly done = signal(createTasks('done', 200)); readonly taskId = (task: Task): string => task.id; // (drop) fires on the destination list only, so both lists bind it. onDrop(event: DropEvent): void { moveItem(event, { todo: this.todo, done: this.done }); } } ``` Three details make this work: - Both `` elements are inside the same `vdndGroup`, and so is the shared item template. - **Both lists bind `(drop)`.** The event fires on the destination list only, so a list without a handler silently rejects items. - `moveItem()` receives every list, keyed by `droppableId`. It handles moves within a list as well as between lists. If a droppable ID is missing from the map, the drop is ignored and a warning is logged in dev mode. ## A board with any number of columns Keep one signal per column and build the map for `moveItem()` from your column list: ```ts interface Column { id: string; title: string; tasks: WritableSignal; } readonly columns: Column[] = [ { id: 'todo', title: 'To do', tasks: signal([]) }, { id: 'doing', title: 'In progress', tasks: signal([]) }, { id: 'done', title: 'Done', tasks: signal([]) }, ]; readonly lists = Object.fromEntries(this.columns.map((c) => [c.id, c.tasks])); onDrop(event: DropEvent): void { moveItem(event, this.lists); } ``` ```angular-html
{{ task.title }}
@for (column of columns; track column.id) {

{{ column.title }}

}
``` Draggable IDs must be unique across **all** columns, because drag state is shared across the whole app. ## Controlling where items can go | Goal | How | | --------------------------------------- | ---------------------------------------------------------------------------- | | Lists that never exchange items | Put them in different groups (separate `vdndGroup` elements) | | A list that temporarily accepts nothing | `[disabled]="true"` on the list or on `vdndDroppable` | | Items that cannot be moved | `[disabled]="true"` on the draggable | | Accept only some items | Bind `disabled` from the drag state, see [below](#accepting-only-some-items) | A disabled droppable is skipped completely. The pointer passes through it to whatever is underneath, and the keyboard's left and right arrows skip it. See [Drag behavior](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md#disabling). Releasing over nothing, or over a disabled list, emits no `(drop)`. The item returns to where it started. ## Accepting only some items There is no predicate input that rejects some items. Returning early from `(drop)` keeps your data unchanged, but during the drag the target list still opens a slot for the item, so the user sees a drop that then does not happen. Instead, disable the list while an item it does not accept is being dragged. `DragStateService` tells you which item that is, and a droppable whose `disabled` changes mid-drag is skipped from the next frame: ```ts readonly #dragState = inject(DragStateService); // Only tasks with an assignee can move to "Done" readonly doneRejectsDrag = computed(() => { const task = this.#dragState.draggedItem()?.data as Task | undefined; return task !== undefined && !task.assignee; }); ``` ```angular-html
{{ task.title }}
``` The pointer then passes over the list without a placeholder, and the keyboard's left and right arrows skip it. Style `.vdnd-droppable-disabled` to show that the list does not accept the item. --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/choosing-a-component.md --- > 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. # Choosing a component The first three options support dynamic heights, pointer auto-scroll and moving items between lists. They differ in how much markup you control and in which element scrolls. Only `` and `` scroll to follow a keyboard drag. | You want | Use | | ---------------------------------------------------------------- | ------------------------------------------------------------------ | | A sortable list with the least code | [``](#sortable-list) | | Your own markup around the scroller, or programmatic scrolling | [`vdndDroppable` + ``](#low-level-scroller) | | A list that scrolls with the page, or inside Ionic `ion-content` | [`vdndScrollable` + ``](#page-level-scroll) | | A self-scrolling building block for a structural directive | [`` + `*vdndVirtualFor`](#virtual-viewport) | ## Sortable list The default. One element combines the droppable, the virtual scroller and the placeholder. It also keeps the dragged item rendered while it is scrolled out of view. See the [Quick start](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/quick-start.md). ```angular-html ``` ## Low-level scroller Put `vdndDroppable` on your own element and place `` inside it. Use this when you need a wrapper with its own layout, the `stickyItemIds` input, or the scrolling methods (`scrollTo`, `scrollToIndex`, `scrollBy`). ```angular-html
{{ task.title }}
``` Pass the same ID to `vdndDroppable` and to `droppableId` on ``. The scroller needs it to show the placeholder. To scroll from code, get the component with `viewChild(VirtualScrollContainerComponent)`. ## Page-level scroll When the list scrolls together with the page, headers and footers, the scroll container is an element you own. Mark it with `vdndScrollable` and render items with `` and `*vdndVirtualFor`. See [Page-level scrolling](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/page-scroll.md). ## Virtual viewport `` is a lower-level building block: a self-scrolling container for `*vdndVirtualFor` that positions all rows with a single transform. Children inherit `itemHeight` and `dynamicItemHeight` from it. For drag and drop, put `vdndDroppable` on the viewport itself; `*vdndVirtualFor` inherits its ID. ```angular-html
{{ task.title }}
``` Give the viewport a height with CSS (for example `.viewport { height: 400px; }`). --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/features/dynamic-heights.md --- > 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. # 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 heights[Open in new tab](/ngx-virtual-dnd/demo/examples/dynamic-height/) ```ts file="/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: `
{{ note.title }} {{ note.body }}
`, }) export class DynamicHeightExampleComponent { readonly notes = signal( 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](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.md#rules-that-fail-silently) 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: ```angular-html ... ... ``` Inside `` or ``, `*vdndVirtualFor` inherits `itemHeight` and `dynamicItemHeight` from its parent. Directly inside a `vdndScrollable`, it positions each row absolutely at its measured offset (see [VirtualForDirective](https://gultyayev.github.io/ngx-virtual-dnd/api/directives.md#virtualfordirective)). --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/features/page-scroll.md --- > 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. # 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 `` and `*vdndVirtualFor`. Live exampleList that scrolls with its header and footer[Open in new tab](/ngx-virtual-dnd/demo/examples/page-scroll/) ```ts file="/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: `
{{ task.title }}
Regular content after the list
`, }) export class PageScrollExampleComponent { readonly tasks = signal( 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: ```css .page { height: 100vh; /* the example uses 400px */ overflow: auto; } ``` - `` must be inside the `vdndScrollable` element. - Inside ``, `*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: ```angular-html
...
...
``` ```css .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. ``, `` and `` 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 ``, the rows start `contentOffset` down when that is more than the top inset. ## Ionic `` 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: ```angular-html
``` ```css .ion-content-scroll-host { height: 100%; overflow-y: auto; } ``` The [page scroll demo](https://gultyayev.github.io/ngx-virtual-dnd/demo/page-scroll) is a complete Ionic task manager built this way. --- url: https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md --- > 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. # Drag behavior These inputs change when a drag starts and how it moves. All of them are optional. Only one drag runs at a time. While an item is being dragged, with the pointer or the keyboard, pressing another item (a second finger on a touch screen) or pressing Space on it does not start a second drag. ## Drag handles Set `dragHandle` to a CSS selector, and only presses inside a matching element in the row start a drag (an element around the row that matches doesn't count). The rest of the row stays free for selecting text, clicking and so on. Live exampleList with drag handles[Open in new tab](/ngx-virtual-dnd/demo/examples/drag-handle/) ```ts file="/src/app/examples/drag-handle/drag-handle.ts" import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { DragPreviewComponent, DraggableDirective, DropEvent, DroppableGroupDirective, reorderItems, VirtualSortableListComponent, } from 'ngx-virtual-dnd'; interface Track { id: string; title: string; } @Component({ selector: 'app-drag-handle-example', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ VirtualSortableListComponent, DroppableGroupDirective, DraggableDirective, DragPreviewComponent, ], template: `
{{ track.title }}
`, }) export class DragHandleExampleComponent { readonly tracks = signal( Array.from({ length: 300 }, (_, i) => ({ id: `track-${i + 1}`, title: `Track ${i + 1}` })), ); readonly trackId = (track: Track): string => track.id; onDrop(event: DropEvent): void { reorderItems(event, this.tracks); } } ``` Make the handle a non-interactive element such as a ``. Presses inside a button in the row never start a drag, as the next section explains. Keyboard dragging is unaffected: the whole row stays focusable. ## Interactive children Controls inside rows keep working: pressing them never starts a drag. What starts a drag depends on where the pointer press lands, or on which element has focus when Space is pressed: | Element | Pointer press starts a drag | Space while it has focus picks the row up | | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------- | | The row itself, even when the draggable element is a button, such as `