umbraco-sorter

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "umbraco-sorter" with this command: npx skills add umbraco/umbraco-cms-backoffice-skills/umbraco-umbraco-cms-backoffice-skills-umbraco-sorter

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

State Management: For reactive updates when order changes

  • Reference skill: umbraco-state-management

Umbraco Element: For creating sortable item elements

  • Reference skill: umbraco-umbraco-element

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, { // Get unique identifier from DOM element getUniqueOfElement: (element) => { return element.getAttribute('data-id') ?? ''; }, // Get unique identifier from data model getUniqueOfModel: (modelEntry) => { return modelEntry.id; }, // Identifier shared by all connected sorters (for cross-container dragging) identifier: 'my-sortable-list', // CSS selector for sortable items itemSelector: '.sortable-item', // CSS selector for the container containerSelector: '.sortable-container', // Called when order changes onChange: ({ model }) => { this._items = model; this.requestUpdate(); this.dispatchEvent(new CustomEvent('change', { detail: { items: model } })); }, });

@property({ type: Array, attribute: false }) public get items(): MyItem[] { return this._items; } public set items(value: MyItem[]) { this._items = value; this.#sorter.setModel(value); this.requestUpdate(); } private _items: MyItem[] = [];

override render() { return html &#x3C;div class="sortable-container"> ${repeat( this._items, (item) => item.id, (item) => html <div class="sortable-item" data-id=${item.id}> ${item.name} </div> )} &#x3C;/div> ; } }

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, // IMPORTANT: Same identifier allows items to move between all nested groups identifier: 'my-nested-sorter', itemSelector: 'my-sorter-item', containerSelector: '.sorter-container', onChange: ({ model }) => { const oldValue = this._value; this._value = model; this.requestUpdate('value', oldValue); this.dispatchEvent(new CustomEvent('change')); }, });

@property({ type: Array, attribute: false }) public get value(): NestedItem[] { return this._value ?? []; } public set value(value: NestedItem[]) { this._value = value; this.#sorter.setModel(value); this.requestUpdate(); } private _value?: NestedItem[];

override render() { return html &#x3C;div class="sorter-container"> ${repeat( this.value, (item) => item.name, (item) => html <my-sorter-item .name=${item.name}> <!-- Recursive nesting --> <my-sorter-group .value=${item.children ?? []} @change=${(e: Event) => { item.children = (e.target as MySorterGroupElement).value; }} ></my-sorter-group> </my-sorter-item> )} &#x3C;/div> ; }

static override styles = css :host { display: block; min-height: 20px; border: 1px dashed rgba(122, 122, 122, 0.25); border-radius: var(--uui-border-radius); padding: var(--uui-size-space-1); } ; }

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 &#x3C;div class="item-wrapper"> &#x3C;div class="drag-handle"> &#x3C;uui-icon name="icon-navigation">&#x3C;/uui-icon> &#x3C;/div> &#x3C;div class="item-content"> &#x3C;span>${this.name}&#x3C;/span> &#x3C;slot name="action">&#x3C;/slot> &#x3C;/div> &#x3C;div class="children"> &#x3C;slot>&#x3C;/slot> &#x3C;/div> &#x3C;/div> ; }

static override styles = css` :host { display: block; background: var(--uui-color-surface); border: 1px solid var(--uui-color-border); border-radius: var(--uui-border-radius); margin: var(--uui-size-space-1) 0; }

.item-wrapper {
  padding: var(--uui-size-space-3);
}

.drag-handle {
  cursor: grab;
  display: inline-block;
  margin-right: var(--uui-size-space-2);
}

.drag-handle:active {
  cursor: grabbing;
}

.children {
  margin-left: var(--uui-size-space-5);
  margin-top: var(--uui-size-space-2);
}

`; }

declare global { interface HTMLElementTagNameMap { 'my-sorter-item': MySorterItemElement; } }

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>

    &#x3C;my-sortable-list
      .items=${this.listTwoItems}
      @change=${(e: CustomEvent) => {
        this.listTwoItems = e.detail.items;
      }}
    >&#x3C;/my-sortable-list>
  &#x3C;/div>
`;

} }

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

// Set the model (call when items change externally) this.#sorter.setModel(items);

// Get current model const currentItems = this.#sorter.getModel();

// Disable sorting temporarily this.#sorter.disable();

// Re-enable sorting 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

umbraco-backoffice

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-dashboard

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-quickstart

No summary provided by upstream source.

Repository SourceNeeds Review
General

umbraco-extension-template

No summary provided by upstream source.

Repository SourceNeeds Review