Angular

Reorder Angular Mat-Table Columns: 2025 Guide in 5 Steps

Tired of static tables? Learn how to let users reorder Angular Mat-Table columns with our easy 5-step guide. Updated for 2025 with drag-and-drop examples!

E

Elena Petrova

Senior Frontend Engineer specializing in Angular and creating dynamic user experiences.

7 min read18 views

Tired of Rigid Tables? Let Your Users Take Control

Let’s be honest: in the world of web applications, data tables are everywhere. They’re the workhorses of dashboards, admin panels, and reports. But for years, they’ve been notoriously static. You, the developer, decide the column order, and the user is stuck with it. What if they want to see the customer’s email right next to their name? Or move a less important column to the very end?

In the past, implementing features like column reordering was a complex, custom-coded nightmare. It involved clunky JavaScript, manual DOM manipulation, and a high risk of introducing bugs. Thankfully, those days are over. The Angular team has given us a powerful and elegant solution right out of the box: the Angular Component Dev Kit (CDK).

In this 2025 guide, we’ll walk you through exactly how to empower your users by adding drag-and-drop column reordering to your Angular Material tables. In just five simple steps, you’ll transform your static `mat-table` into a dynamic, interactive, and user-friendly component that feels modern and intuitive. Let’s dive in!

Step 1: Setting Up Your Project with the CDK Drag & Drop Module

Before we can work our magic, we need to ensure our project has the necessary tools. This guide assumes you already have an Angular project with Angular Material installed. If not, you can follow the official Angular Material setup guide first.

The key ingredient for our functionality is the DragDropModule from the Angular CDK. It's a separate package that provides a suite of tools for building complex UI interactions, and its drag-and-drop functionality is incredibly robust.

First, make sure you have @angular/cdk installed. If you installed Angular Material with ng add @angular/material, you should already have it. If not, you can install it via npm:

npm install @angular/cdk

Next, you need to import the DragDropModule into the module where your table component is declared. For most standalone component applications or simple projects, this will be your main `app.component.ts` or the specific component's file. If you are using NgModules, it will be your `app.module.ts` or a relevant feature module.

Here’s how you’d add it to your imports array:

// In your component's imports array (for standalone components)
// or in your @NgModule imports array

import { DragDropModule } from '@angular/cdk/drag-drop';

// ...

@Component({
  // ...
  standalone: true,
  imports: [
    // ... other modules like MatTableModule
    DragDropModule 
  ],
})
export class MyTableComponent { /* ... */ }

With this one import, you've unlocked all the directives and services you'll need for the next steps. Easy, right?

Step 2: Defining Your Columns for Dynamic Rendering

The secret to a reorderable table lies in how you define and use your column list. A static, hardcoded header row in your HTML won't work. Instead, we need to drive the column rendering from a single source of truth in our component’s TypeScript file: the displayedColumns array.

In your component's .ts file, define an array of strings. Each string in this array corresponds to the name you give your column definitions in the HTML template (using matColumnDef).

Let's set up a simple example with some periodic elements:

// my-table.component.ts

import { Component } from '@angular/core';
// ... other imports

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: PeriodicElement[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  // ... more data
];

@Component({
  selector: 'app-my-table',
  // ...
})
export class MyTableComponent {
  // This array now controls the order of the columns
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;
}

Notice how displayedColumns is just a simple array of strings. When we later reorder this array, Angular's change detection will automatically update the table in the DOM to reflect the new order. This is the core principle that makes the whole process so clean.

Your corresponding HTML will use this displayedColumns array in the mat-header-row and mat-row definitions:

Advertisement
<!-- my-table.component.html -->

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

  <!-- Position Column -->
  <ng-container matColumnDef="position">
    <th mat-header-cell *matHeaderCellDef> No. </th>
    <td mat-cell *matCellDef="let element"> {{element.position}} </td>
  </ng-container>

  <!-- Name Column -->
  <ng-container matColumnDef="name">
    <th mat-header-cell *matHeaderCellDef> Name </th>
    <td mat-cell *matCellDef="let element"> {{element.name}} </td>
  </ng-container>

  <!-- ... other column definitions for 'weight' and 'symbol' ... -->

  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

At this point, you have a standard, functional `mat-table`. Now for the fun part.

Step 3: Making the Header Row Draggable

This is where we connect the CDK to our table. We need to tell the CDK which elements are part of a draggable group and which specific items can be dragged.

We'll modify the mat-header-row and its child mat-header-cell elements.

  1. Wrap the `mat-header-row` in a `cdkDropList` container. This directive defines the boundary within which items can be dragged and dropped. Since we are reordering horizontally, we must set its orientation.
  2. Add a `(cdkDropListDropped)` event emitter. This will call a method in our component whenever a user drops a column.
  3. Apply the `cdkDrag` directive to each `mat-header-cell`. This marks each header cell as a draggable item.

Let's update the HTML. The key change is to the table's header section:

<!-- ... inside your mat-table ... -->

  <!-- ... your ng-container column definitions are here ... -->

  <tr mat-header-row *matHeaderRowDef="displayedColumns"
      cdkDropList
      cdkDropListOrientation="horizontal"
      (cdkDropListDropped)="drop($event)">
  </tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

<!-- Now, update each header cell to be draggable -->
<!-- Example for the 'position' column -->
<ng-container matColumnDef="position">
  <th mat-header-cell *matHeaderCellDef cdkDrag> No. </th> <!-- Add cdkDrag here -->
  <td mat-cell *matCellDef="let element"> {{element.position}} </td>
</ng-container>

<!-- Example for the 'name' column -->
<ng-container matColumnDef="name">
  <th mat-header-cell *matHeaderCellDef cdkDrag> Name </th> <!-- And here -->
  <td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>

<!-- ... DO THIS FOR ALL YOUR COLUMN DEFINITIONS ... -->

Important: You must apply the `cdkDrag` directive to the `` elements *inside* each `ng-container[matColumnDef]`. Applying it to the `ng-container` itself will not work correctly.

If you run your app now, you'll see a console error when you try to drag, because we haven't defined the `drop()` method yet. Let's fix that.

Step 4: Implementing the Reorder Logic in Your Component

All the UI setup is done. Now we just need to write a few lines of TypeScript to handle the reordering logic. The CDK makes this incredibly simple.

When the `(cdkDropListDropped)` event fires, it passes an event object of type `CdkDragDrop`. This object contains everything we need, most importantly `previousIndex` and `currentIndex`.

The CDK also provides a handy utility function called `moveItemInArray`. It does exactly what its name suggests: it moves an item in an array from one index to another. This is perfect for manipulating our `displayedColumns` array.

In your component's .ts file, import `CdkDragDrop` and `moveItemInArray`, then create the `drop` method:

// my-table.component.ts

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

@Component({ /* ... */ })
export class MyTableComponent {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(this.displayedColumns, event.previousIndex, event.currentIndex);
  }
}

And that's it. Seriously.

When you drag a column header and drop it in a new position, the `drop` function is called. `moveItemInArray` rearranges the strings in your `displayedColumns` array. Because the `mat-header-row` and `mat-row` directives are bound to this array, Angular detects the change and re-renders the table columns in their new order. The entire table, including all its data rows, instantly reflects the new column sequence.

Step 5: Adding Visual Polish and User Feedback

Functionality is great, but a polished user experience is what makes an application feel professional. The CDK helps us here, too, by automatically adding CSS classes to elements during the drag-and-drop lifecycle.

We can style these classes to provide clear visual feedback to the user.

  • .cdk-drag-preview: A clone of the dragged element that follows the user's cursor. We can make it semi-transparent and add a box shadow to "lift" it off the page.
  • .cdk-drag-placeholder: An empty placeholder that appears in the drop list to show where the item will land. We can give it a subtle background.
  • .cdk-drop-list-dragging: A class added to the drop list while an item from it is being dragged. We can use this to change the cursor.

Add the following styles to your component's CSS file (e.g., `my-table.component.scss`):

/* Change cursor to 'grabbing' while a column is being dragged */
.cdk-drop-list-dragging .cdk-drag {
  cursor: grabbing;
}

/* The user's mouse cursor */
.cdk-drag-preview {
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
              0 8px 10px 1px rgba(0, 0, 0, 0.14),
              0 3px 14px 2px rgba(0, 0, 0, 0.12);
  opacity: 0.8;
}

/* The placeholder element that shows where the item will be dropped */
.cdk-drag-placeholder {
  background: #ccc;
  border: dotted 3px #999;
  min-height: 60px;
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
  opacity: 0.3;
}

These simple styles make a huge difference. The user now gets a grabbing cursor, a semi-transparent preview of the column they're moving, and a clear placeholder showing where it will land. It's these small details that create a truly intuitive experience.

Bonus: Persisting the Column Order

You've built a fantastic feature, but there's one catch: if the user reloads the page, the column order will reset to the default. To truly delight your users, you should save their preferred order.

The easiest way to do this is with the browser's `localStorage`.

1. Save the order after a drop:

Modify your `drop` function to save the newly ordered `displayedColumns` array to `localStorage`.

drop(event: CdkDragDrop<string[]>) {
  moveItemInArray(this.displayedColumns, event.previousIndex, event.currentIndex);
  // Save the new order to localStorage
  localStorage.setItem('tableColumnOrder', JSON.stringify(this.displayedColumns));
}

2. Load the order on initialization:

When the component is created, check if a saved order exists in `localStorage`. If it does, use it to initialize `displayedColumns`.

We can do this in the component's constructor or `ngOnInit` lifecycle hook.

export class MyTableComponent implements OnInit {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;

  constructor() {}

  ngOnInit(): void {
    const savedOrder = localStorage.getItem('tableColumnOrder');
    if (savedOrder) {
      this.displayedColumns = JSON.parse(savedOrder);
    }
  }

  // ... drop function ...
}

With these two small additions, your application will now remember the user's column layout between sessions, creating a personalized and much more powerful user experience.

Conclusion

You've done it! In just five steps, you’ve transformed a standard Angular Material table into a fully interactive component. By leveraging the power of the Angular CDK's `DragDropModule` and binding your table's structure to a dynamic array, you've unlocked a feature that provides immense value to your users with surprisingly little code.

We've covered setting up the module, structuring your data, implementing the drag-and-drop directives, writing the core reordering logic, and adding the visual polish that makes the feature intuitive. As a bonus, you even learned how to persist the user's settings using `localStorage`. This is a perfect example of how the Angular ecosystem provides powerful, well-designed tools that let you build sophisticated features efficiently. Now go ahead and give your users the control they deserve!

Tags

You May Also Like