DEV Community

Cover image for Make Any List Draggable in Minutes
Brian Treese
Brian Treese

Posted on • Originally published at briantree.se

Make Any List Draggable in Minutes

Ever built a list in Angular and thought, "It’d be awesome if I could just drag these items around to reorder them?" Well, what if I told you that you can? And it’s ridiculously easy! In this tutorial, we’ll add drag-to-reorder functionality to a todo list using Angular’s CDK. No extra libraries, no complex setup, just clean, modern Angular.

By the end, you’ll have a todo list you can reorder effortlessly using simple directives and one helper function.

Let’s jump in!

Tour the Todo App (Before Drag and Drop)

Here’s our little app, a simple todo list:

The todo list before drag and drop functionality

We can check and uncheck items, and as we do, the remaining count updates right in the header.

That’s powered by a signal, so Angular automatically updates the DOM whenever the value changes.

Now, it looks like you could drag these items around:

The todo list with the grip icon and grab cursor but no drag and drop functionality

I mean, it even has the little "grip" icon and we get the grab cursor when hovering, but if we try to move one… nothing happens.

Not yet, anyway.

This is what we’ll fix in this tutorial, but first, let’s review the existing code to understand what we’re working with.

How the Angular Todo List Works

In the component’s template, we’ve got a header showing the total task count and remaining count:

<header class="header">
    ...
    <p class="subtle">
        Tasks: {{ todos().length }} • Remaining: {{ remainingCount() }}
    </p>
</header>
Enter fullscreen mode Exit fullscreen mode

That’s what you see above the list:

The header of the todo list displaying the total tasks and remaining tasks

Then we’ve got the actual list of todos, rendered using a @for block that creates a list item for each todo in the array:

<ul class="todo-list">
    @for (todo of todos(); track todo.id; let i = $index) {
        <li class="todo-item">
            ...
        </li>
    }
</ul>
Enter fullscreen mode Exit fullscreen mode

This is the section we’ll make draggable so we can reorder it.

In the TypeScript, we have a signal called todos, which is just an array of Todo objects:

type Todo = { id: string; title: string; done?: boolean };

protected todos = signal<Todo[]>([
    { id: '1', title: 'Pay invoices' },
    { id: '2', title: 'Email onboarding checklist' },
    { id: '3', title: 'Review pull request' },
    { id: '4', title: 'Prep sprint demo' },
]);
Enter fullscreen mode Exit fullscreen mode

We also have a remainingCount computed signal that filters out completed tasks whenever the todos change:

protected remainingCount = computed(() =>
    this.todos().filter(t => !t.done).length);
Enter fullscreen mode Exit fullscreen mode

Finally, there’s a simple toggle function that flips the done state when you click a checkbox:

protected toggle(t: Todo) {
    this.todos.update(list =>
        list.map(item => (item.id === t.id ? 
            { ...item, done: !item.done } : item))
    );
}
Enter fullscreen mode Exit fullscreen mode

So overall, it’s clean, reactive, and modern, but the list order is static.

Let’s fix that.

Step 1: Install the Angular CDK for Drag and Drop

The Angular CDK provides low-level utilities like accessibility helpers, overlays, and in our case, drag-and-drop.

Before we can use it, we just need to install it from within the root of our Angular app using the following command:

npm install @angular/cdk
Enter fullscreen mode Exit fullscreen mode

Once installed, we’re ready to start using CDK directives directly in our component.

Step 2: Import the CDK Directives

Next, we’ll open the component TypeScript and import the CDK directives we need.

For drag-and-drop, we’ll import two directives: the CDKDropList and CDKDrag:

import { CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';

@Component({
    selector: 'app-root',
    ...,
    imports: [CdkDropList, CdkDrag],
})
Enter fullscreen mode Exit fullscreen mode

These imports make the directives available in our template so we can turn any element into a droppable list or a draggable item.

Step 3: Turn the List into a Drop Zone

Now, let’s make our list droppable.

We’ll add the cdkDropList directive to the list container:

<ul
    cdkDropList
    class="todo-list">
    ...
</ul>
Enter fullscreen mode Exit fullscreen mode

This turns the element into a "drop zone".

Basically, a container that manages draggable items inside it.

Then, we can listen to the cdkDropListDropped event, which fires every time an item is dragged and dropped.

We’ll call a drop() function and pass along the event to handle reordering:

<ul
    cdkDropList
    (cdkDropListDropped)="drop($event)"
    class="todo-list">
    ...
</ul>
Enter fullscreen mode Exit fullscreen mode

Step 4: Make Each Todo Item Draggable

Next up, let’s make each todo draggable.

Each <li> represents one todo, so we’ll add the cdkDrag directive there:

<li
    cdkDrag
    class="todo-item">
    ...
</li>
Enter fullscreen mode Exit fullscreen mode

That’s all it takes.

This single attribute allows Angular to track and drag that element.

You don’t even need a drag handle, the entire item can be dragged by default.

But since we’ve already got that fancy little grip icon, you could turn that into a handle later if you want.

Step 5: Handle the Drop Event and Reorder the List

Now let’s head back to the TypeScript and add our drop() function:

import { ..., CdkDragDrop } from '@angular/cdk/drag-drop';

protected drop(event: CdkDragDrop<Todo[]>) {
}
Enter fullscreen mode Exit fullscreen mode

This function receives a special CDKDragDrop event when the drag operation finishes.

Inside, we’ll create a new copy of the todos array (to avoid mutating the existing reference):

protected drop(event: CdkDragDrop<Todo[]>) {
    const next = [...this.todos()];
}
Enter fullscreen mode Exit fullscreen mode

Angular signals detect changes best when a new reference is returned.

Then, we’ll call the CDK helper function moveItemInArray():

import { ..., moveItemInArray } from '@angular/cdk/drag-drop';

protected drop(event: CdkDragDrop<Todo[]>) {
    const next = [...this.todos()];
    moveItemInArray(next, event.previousIndex, event.currentIndex);
}
Enter fullscreen mode Exit fullscreen mode

It takes care of moving the dragged item from its previous index to its new position, no manual index juggling or splice logic required.

Then, we just return the new array:

protected drop(event: CdkDragDrop<Todo[]>) {
    const next = [...this.todos()];
    moveItemInArray(next, event.previousIndex, event.currentIndex);
    return next;
}
Enter fullscreen mode Exit fullscreen mode

And that’s it.

Our drop function is now complete.

Step 6: Test Drag and Drop Reordering

Let’s save everything and test it out:

The todo list with drag and drop functionality using the Angular CDK

Now when we click and drag one of the tasks, look at that!

It moves smoothly, and when we drop it, Angular re-renders the list in the new order.

We just added drag-to-reorder functionality with one helper method and two directives.

That’s an amazing tradeoff.

Step 7: Add Visual Feedback

Right now it works great, but it’s not super clear what’s happening during a drag.

Let’s fix that with a couple of small style tweaks.

In the component’s stylesheet, we can style two special classes that the CDK automatically adds during drag operations:

First, we'll add styles using the cdk-drag-preview class:

.cdk-drag-preview {
    background: #111827;
    color: #fff;
    border-radius: 10px;
    padding: 8px 12px;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}
Enter fullscreen mode Exit fullscreen mode

This is applied to the item being dragged (the floating element that follows your cursor).

This preview will have a darker background and a subtle drop shadow for depth.

Next, we'll add styles using the cdk-drag-placeholder class:

.cdk-drag-placeholder {
    background-color: yellow;
    border: 2px dashed #c7d2fe;
    border-radius: 10px;
    height: 44px;
}
Enter fullscreen mode Exit fullscreen mode

This is applied to the space where the dragged item will drop.

This placeholder will be bright yellow so it’s easy to see exactly where the item will land.

We’re going for maximum clarity here, not subtlety.

Okay, now let’s save and try it again!

Final Test: The Polished Drag-and-Drop Experience

The todo list with drag and drop functionality using the Angular CDK and visual feedback

When we drag now, the preview follows our mouse with a dark background, and the yellow placeholder clearly shows the drop location.

It’s so much easier to see what’s happening.

Wrap-Up: Drag to Reorder Made Easy

And that’s it! We added drag-to-reorder functionality to a todo list using Angular’s CDK.

We...

  • Installed the CDK
  • Added the CdkDropList and CdkDrag directives
  • Handled the dropped event
  • Used moveItemInArray() to reorder our data
  • Finished with visual feedback for a smooth experience

You can take this same approach to reorder anything, from cards in a kanban board to rows in a table, or even playlists in a music app.

If you found this helpful, be sure to subscribe for more quick Angular tips!

And maybe even reorder your todo list so "Keep an eye out for Brian’s next tutorial" is at the top!

Additional Resources

Try It Yourself

Want to experiment with the final version? Explore the full StackBlitz demo below.

If you have any questions or thoughts, don’t hesitate to leave a comment.

Top comments (0)