| name | umbraco-sorter |
| description | Implement drag-and-drop sorting with UmbSorterController in Umbraco backoffice |
| version | 1.0.0 |
| location | managed |
| allowed-tools | Read, Write, Edit, WebFetch |
Umbraco Sorter
What is it?
The UmbSorterController provides drag-and-drop sorting functionality for lists of items in the Umbraco backoffice. It handles reordering items within a container, moving items between containers, and supports nested sorting scenarios. This is useful for block editors, content trees, and any UI that requires user-driven ordering.
Documentation
Always fetch the latest docs before implementing:
Reference Examples
The Umbraco source includes working examples:
Nested Containers: /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/
This example demonstrates nested sorting with items that can contain child items.
Two Containers: /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/
This example shows moving items between two separate containers.
Related Foundation Skills
Workflow
- Fetch docs - Use WebFetch on the URLs above
- Ask questions - Single or multiple containers? Nested items? What data model?
- Generate files - Create container element + item element + sorter setup
- Explain - Show what was created and how sorting works
Basic Sorter Setup
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
interface MyItem {
id: string;
name: string;
}
@customElement('my-sortable-list')
export class MySortableListElement extends UmbLitElement {
#sorter = new UmbSorterController<MyItem, HTMLElement>(this, {
getUniqueOfElement: (element) => {
return element.getAttribute('data-id') ?? '';
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'my-sortable-list',
: ,
: ,
: {
. = model;
.();
.( (, { : { : model } }));
},
});
({ : , : })
(): [] {
.;
}
() {
. = value;
.#sorter.(value);
.();
}
: [] = [];
() {
html`;
}
}
Nested Sorter (Items with Children)
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export interface NestedItem {
name: string;
children?: NestedItem[];
}
@customElement('my-sorter-group')
export class MySorterGroupElement extends UmbLitElement {
#sorter = new UmbSorterController<NestedItem, MySorterItemElement>(this, {
getUniqueOfElement: (element) => element.name,
getUniqueOfModel: (modelEntry) => modelEntry.name,
identifier: 'my-nested-sorter',
itemSelector: 'my-sorter-item',
containerSelector: '.sorter-container',
onChange: ({ model }) => {
oldValue = .;
. = model;
.(, oldValue);
.( ());
},
});
({ : , : })
(): [] {
. ?? [];
}
() {
. = value;
.#sorter.(value);
.();
}
?: [];
() {
html`;
}
styles = css`;
}
Sortable Item Element
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('my-sorter-item')
export class MySorterItemElement extends UmbLitElement {
@property({ type: String })
name = '';
override render() {
return html`
<div class="item-wrapper">
<div class="drag-handle">
<uui-icon name="icon-navigation"></uui-icon>
</div>
<div class="item-content">
<span>${this.name}</span>
<slot name="action"></slot>
</>
`;
}
styles = css`;
}
{
{
: ;
}
}
Two Containers (Cross-Container Sorting)
@customElement('my-dual-sorter-dashboard')
export class MyDualSorterDashboard extends UmbLitElement {
listOneItems: MyItem[] = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
];
listTwoItems: MyItem[] = [
{ id: '3', name: 'Carrot' },
{ id: '4', name: 'Date' },
];
override render() {
return html`
<div class="container">
<my-sortable-list
.items=${this.listOneItems}
@change=${(e: CustomEvent) => {
this.listOneItems = e.detail.items;
}}
></my-sortable-list>
<my-sortable-list
.items=${this.listTwoItems};
}
}
Key: Both lists use the same identifier in their UmbSorterController to enable dragging between them.
UmbSorterController Options
| Option | Type | Description |
|---|
identifier | string | Shared ID for connected sorters (enables cross-container dragging) |
itemSelector | string | CSS selector for sortable items |
containerSelector | string | CSS selector for the container |
getUniqueOfElement | (element) => string | Extract unique ID from DOM element |
getUniqueOfModel | (model) => string | Extract unique ID from data model |
onChange | ({ model }) => void | Called when order changes |
onStart | () => void | Called when dragging starts |
onEnd | () => void | Called when dragging ends |
Key Methods
this.#sorter.setModel(items);
const currentItems = this.#sorter.getModel();
this.#sorter.disable();
this.#sorter.enable();
CSS Classes Applied During Drag
| Class | Applied To | When |
|---|
.umb-sorter-dragging | Container | While any item is being dragged |
.umb-sorter-placeholder | Placeholder element | Indicates drop position |
Best Practices
- Use unique identifiers - Each item must have a unique ID
- Match selectors carefully -
itemSelector and containerSelector must match your DOM
- Share identifier - Use same
identifier for connected sorters
- Handle nested updates - Propagate changes up through nested structures
- Use repeat directive - Always use
repeat() with a key function for proper DOM diffing
- Provide visual feedback - Style drag handles and drop zones clearly
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.