diff --git a/app/Sorting/SortSet.php b/app/Sorting/SortSet.php
index 42e1e0951..ee45c211f 100644
--- a/app/Sorting/SortSet.php
+++ b/app/Sorting/SortSet.php
@@ -15,21 +15,21 @@ use Illuminate\Database\Eloquent\Model;
 class SortSet extends Model
 {
     /**
-     * @return SortSetOption[]
+     * @return SortSetOperation[]
      */
-    public function getOptions(): array
+    public function getOperations(): array
     {
         $strOptions = explode(',', $this->sequence);
-        $options = array_map(fn ($val) => SortSetOption::tryFrom($val), $strOptions);
+        $options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions);
         return array_filter($options);
     }
 
     /**
-     * @param SortSetOption[] $options
+     * @param SortSetOperation[] $options
      */
-    public function setOptions(array $options): void
+    public function setOperations(array $options): void
     {
-        $values = array_map(fn (SortSetOption $opt) => $opt->value, $options);
+        $values = array_map(fn (SortSetOperation $opt) => $opt->value, $options);
         $this->sequence = implode(',', $values);
     }
 }
diff --git a/app/Sorting/SortSetOption.php b/app/Sorting/SortSetOperation.php
similarity index 82%
rename from app/Sorting/SortSetOption.php
rename to app/Sorting/SortSetOperation.php
index bb878cf30..12fda669f 100644
--- a/app/Sorting/SortSetOption.php
+++ b/app/Sorting/SortSetOperation.php
@@ -2,7 +2,7 @@
 
 namespace BookStack\Sorting;
 
-enum SortSetOption: string
+enum SortSetOperation: string
 {
     case NameAsc = 'name_asc';
     case NameDesc = 'name_desc';
@@ -34,11 +34,11 @@ enum SortSetOption: string
     }
 
     /**
-     * @return SortSetOption[]
+     * @return SortSetOperation[]
      */
-    public static function allExcluding(array $options): array
+    public static function allExcluding(array $operations): array
     {
-        $all = SortSetOption::cases();
-        return array_diff($all, $options);
+        $all = SortSetOperation::cases();
+        return array_diff($all, $operations);
     }
 }
diff --git a/lang/en/settings.php b/lang/en/settings.php
index b29ec2533..8bb2f6ef4 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -87,7 +87,9 @@ return [
     'sort_set_operations' => 'Sort Operations',
     'sort_set_operations_desc' => 'Configure the sort actions to be performed in this set by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom.',
     'sort_set_available_operations' => 'Available Operations',
+    'sort_set_available_operations_empty' => 'No operations remaining',
     'sort_set_configured_operations' => 'Configured Operations',
+    'sort_set_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
     'sort_set_op_asc' => '(Asc)',
     'sort_set_op_desc' => '(Desc)',
     'sort_set_op_name' => 'Name - Alphabetical',
diff --git a/package-lock.json b/package-lock.json
index 1912106c2..44a735d2f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,7 +4,6 @@
   "requires": true,
   "packages": {
     "": {
-      "name": "bookstack",
       "dependencies": {
         "@codemirror/commands": "^6.7.1",
         "@codemirror/lang-css": "^6.3.1",
@@ -32,6 +31,7 @@
       },
       "devDependencies": {
         "@lezer/generator": "^1.7.2",
+        "@types/sortablejs": "^1.15.8",
         "chokidar-cli": "^3.0",
         "esbuild": "^0.24.0",
         "eslint": "^8.57.1",
@@ -2403,6 +2403,13 @@
         "undici-types": "~6.19.2"
       }
     },
+    "node_modules/@types/sortablejs": {
+      "version": "1.15.8",
+      "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
+      "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/stack-utils": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
diff --git a/package.json b/package.json
index 08af25d14..4571ea77d 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
   },
   "devDependencies": {
     "@lezer/generator": "^1.7.2",
+    "@types/sortablejs": "^1.15.8",
     "chokidar-cli": "^3.0",
     "esbuild": "^0.24.0",
     "eslint": "^8.57.1",
diff --git a/resources/js/components/index.ts b/resources/js/components/index.ts
index 12c991a51..affa25fcf 100644
--- a/resources/js/components/index.ts
+++ b/resources/js/components/index.ts
@@ -50,6 +50,7 @@ export {ShelfSort} from './shelf-sort';
 export {Shortcuts} from './shortcuts';
 export {ShortcutInput} from './shortcut-input';
 export {SortableList} from './sortable-list';
+export {SortSetManager} from './sort-set-manager'
 export {SubmitOnChange} from './submit-on-change';
 export {Tabs} from './tabs';
 export {TagManager} from './tag-manager';
diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js
index 01ca11a33..b56b01980 100644
--- a/resources/js/components/shelf-sort.js
+++ b/resources/js/components/shelf-sort.js
@@ -1,29 +1,6 @@
 import Sortable from 'sortablejs';
 import {Component} from './component';
-
-/**
- * @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
- */
-const itemActions = {
-    move_up(item) {
-        const list = item.parentNode;
-        const index = Array.from(list.children).indexOf(item);
-        const newIndex = Math.max(index - 1, 0);
-        list.insertBefore(item, list.children[newIndex] || null);
-    },
-    move_down(item) {
-        const list = item.parentNode;
-        const index = Array.from(list.children).indexOf(item);
-        const newIndex = Math.min(index + 2, list.children.length);
-        list.insertBefore(item, list.children[newIndex] || null);
-    },
-    remove(item, shelfBooksList, allBooksList) {
-        allBooksList.appendChild(item);
-    },
-    add(item, shelfBooksList) {
-        shelfBooksList.appendChild(item);
-    },
-};
+import {buildListActions, sortActionClickListener} from '../services/dual-lists.ts';
 
 export class ShelfSort extends Component {
 
@@ -55,12 +32,9 @@ export class ShelfSort extends Component {
     }
 
     setupListeners() {
-        this.elem.addEventListener('click', event => {
-            const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
-            if (sortItemAction) {
-                this.sortItemActionClick(sortItemAction);
-            }
-        });
+        const listActions = buildListActions(this.allBookList, this.shelfBookList);
+        const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
+        this.elem.addEventListener('click', sortActionListener);
 
         this.bookSearchInput.addEventListener('input', () => {
             this.filterBooksByName(this.bookSearchInput.value);
@@ -93,20 +67,6 @@ export class ShelfSort extends Component {
         }
     }
 
-    /**
-     * Called when a sort item action button is clicked.
-     * @param {HTMLElement} sortItemAction
-     */
-    sortItemActionClick(sortItemAction) {
-        const sortItem = sortItemAction.closest('.scroll-box-item');
-        const {action} = sortItemAction.dataset;
-
-        const actionFunction = itemActions[action];
-        actionFunction(sortItem, this.shelfBookList, this.allBookList);
-
-        this.onChange();
-    }
-
     onChange() {
         const shelfBookElems = Array.from(this.shelfBookList.querySelectorAll('[data-id]'));
         this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
diff --git a/resources/js/components/sort-set-manager.ts b/resources/js/components/sort-set-manager.ts
new file mode 100644
index 000000000..c35ad41fe
--- /dev/null
+++ b/resources/js/components/sort-set-manager.ts
@@ -0,0 +1,41 @@
+import {Component} from "./component.js";
+import Sortable from "sortablejs";
+import {buildListActions, sortActionClickListener} from "../services/dual-lists";
+
+
+export class SortSetManager extends Component {
+
+    protected input!: HTMLInputElement;
+    protected configuredList!: HTMLElement;
+    protected availableList!: HTMLElement;
+
+    setup() {
+        this.input = this.$refs.input as HTMLInputElement;
+        this.configuredList = this.$refs.configuredOperationsList;
+        this.availableList = this.$refs.availableOperationsList;
+
+        this.initSortable();
+
+        const listActions = buildListActions(this.availableList, this.configuredList);
+        const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
+        this.$el.addEventListener('click', sortActionListener);
+    }
+
+    initSortable() {
+        const scrollBoxes = [this.configuredList, this.availableList];
+        for (const scrollBox of scrollBoxes) {
+            new Sortable(scrollBox, {
+                group: 'sort-set-operations',
+                ghostClass: 'primary-background-light',
+                handle: '.handle',
+                animation: 150,
+                onSort: this.onChange.bind(this),
+            });
+        }
+    }
+
+    onChange() {
+        const configuredOpEls = Array.from(this.configuredList.querySelectorAll('[data-id]'));
+        this.input.value = configuredOpEls.map(elem => elem.getAttribute('data-id')).join(',');
+    }
+}
\ No newline at end of file
diff --git a/resources/js/services/dual-lists.ts b/resources/js/services/dual-lists.ts
new file mode 100644
index 000000000..98f2af92d
--- /dev/null
+++ b/resources/js/services/dual-lists.ts
@@ -0,0 +1,51 @@
+/**
+ * Service for helping manage common dual-list scenarios.
+ * (Shelf book manager, sort set manager).
+ */
+
+type ListActionsSet = Record<string, ((item: HTMLElement) => void)>;
+
+export function buildListActions(
+    availableList: HTMLElement,
+    configuredList: HTMLElement,
+): ListActionsSet {
+    return {
+        move_up(item) {
+            const list = item.parentNode as HTMLElement;
+            const index = Array.from(list.children).indexOf(item);
+            const newIndex = Math.max(index - 1, 0);
+            list.insertBefore(item, list.children[newIndex] || null);
+        },
+        move_down(item) {
+            const list = item.parentNode as HTMLElement;
+            const index = Array.from(list.children).indexOf(item);
+            const newIndex = Math.min(index + 2, list.children.length);
+            list.insertBefore(item, list.children[newIndex] || null);
+        },
+        remove(item) {
+            availableList.appendChild(item);
+        },
+        add(item) {
+            configuredList.appendChild(item);
+        },
+    };
+}
+
+export function sortActionClickListener(actions: ListActionsSet, onChange: () => void) {
+    return (event: MouseEvent) => {
+        const sortItemAction = (event.target as Element).closest('.scroll-box-item button[data-action]') as HTMLElement|null;
+        if (sortItemAction) {
+            const sortItem = sortItemAction.closest('.scroll-box-item') as HTMLElement;
+            const action = sortItemAction.dataset.action;
+            if (!action) {
+                throw new Error('No action defined for clicked button');
+            }
+
+            const actionFunction = actions[action];
+            actionFunction(sortItem);
+
+            onChange();
+        }
+    };
+}
+
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 888b32527..58d39d3ee 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -1062,12 +1062,16 @@ $btt-size: 40px;
     cursor: pointer;
     @include mixins.lightDark(background-color, #f8f8f8, #333);
   }
+  &.items-center {
+    align-items: center;
+  }
   .handle {
     color: #AAA;
     cursor: grab;
   }
   button {
     opacity: .6;
+    line-height: 1;
   }
   .handle svg {
     margin: 0;
@@ -1108,12 +1112,19 @@ input.scroll-box-search, .scroll-box-header-item {
   border-radius: 0 0 3px 3px;
 }
 
-.scroll-box[refs="shelf-sort@shelf-book-list"] [data-action="add"] {
+.scroll-box.configured-option-list [data-action="add"] {
   display: none;
 }
-.scroll-box[refs="shelf-sort@all-book-list"] [data-action="remove"],
-.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_up"],
-.scroll-box[refs="shelf-sort@all-book-list"] [data-action="move_down"],
+.scroll-box.available-option-list [data-action="remove"],
+.scroll-box.available-option-list [data-action="move_up"],
+.scroll-box.available-option-list [data-action="move_down"],
 {
   display: none;
+}
+
+.scroll-box > li.empty-state {
+  display: none;
+}
+.scroll-box > li.empty-state:last-child {
+  display: list-item;
 }
\ No newline at end of file
diff --git a/resources/views/settings/sort-sets/parts/form.blade.php b/resources/views/settings/sort-sets/parts/form.blade.php
index 6df04a721..3f2220947 100644
--- a/resources/views/settings/sort-sets/parts/form.blade.php
+++ b/resources/views/settings/sort-sets/parts/form.blade.php
@@ -1,4 +1,3 @@
-
 <div class="setting-list">
     <div class="grid half">
         <div>
@@ -13,59 +12,36 @@
         </div>
     </div>
 
-    <div>
+    <div component="sort-set-manager">
         <label class="setting-list-label">{{ trans('settings.sort_set_operations') }}</label>
         <p class="text-muted text-small">{{ trans('settings.sort_set_operations_desc') }}</p>
 
-
+        <input refs="sort-set-manager@input" type="hidden" name="books"
+               value="{{ $model?->sequence ?? '' }}">
 
         <div class="grid half">
             <div class="form-group">
-                <label for="books" id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label>
-                <ul refs="sort-set@configured-operations-list"
+                <label for="books"
+                       id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label>
+                <ul refs="sort-set-manager@configured-operations-list"
                     aria-labelledby="sort-set-configured-operations"
-                    class="scroll-box">
-                    @foreach(($model?->getOptions() ?? []) as $option)
-                        <li data-id="{{ $option->value }}"
-                            class="scroll-box-item">
-                            <div class="handle px-s">@icon('grip')</div>
-                            <div>{{ $option->getLabel() }}</div>
-                            <div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
-                                <button type="button" data-action="move_up" class="icon-button p-xxs"
-                                        title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
-                                <button type="button" data-action="move_down" class="icon-button p-xxs"
-                                        title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
-                                <button type="button" data-action="remove" class="icon-button p-xxs"
-                                        title="{{ trans('common.remove') }}">@icon('remove')</button>
-                                <button type="button" data-action="add" class="icon-button p-xxs"
-                                        title="{{ trans('common.add') }}">@icon('add-small')</button>
-                            </div>
-                        </li>
+                    class="scroll-box configured-option-list">
+                    <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_configured_operations_empty') }}</li>
+                    @foreach(($model?->getOperations() ?? []) as $option)
+                        @include('settings.sort-sets.parts.operation')
                     @endforeach
                 </ul>
             </div>
 
             <div class="form-group">
-                <label for="books" id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label>
-                <ul refs="sort-set@available-operations-list"
+                <label for="books"
+                       id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label>
+                <ul refs="sort-set-manager@available-operations-list"
                     aria-labelledby="sort-set-available-operations"
-                    class="scroll-box">
-                    @foreach(\BookStack\Sorting\SortSetOption::allExcluding($model?->getOptions() ?? []) as $option)
-                        <li data-id="{{ $option->value }}"
-                            class="scroll-box-item">
-                            <div class="handle px-s">@icon('grip')</div>
-                            <div>{{ $option->getLabel() }}</div>
-                            <div class="buttons flex-container-row items-center ml-auto px-xxs py-xs">
-                                <button type="button" data-action="move_up" class="icon-button p-xxs"
-                                        title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
-                                <button type="button" data-action="move_down" class="icon-button p-xxs"
-                                        title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
-                                <button type="button" data-action="remove" class="icon-button p-xxs"
-                                        title="{{ trans('common.remove') }}">@icon('remove')</button>
-                                <button type="button" data-action="add" class="icon-button p-xxs"
-                                        title="{{ trans('common.add') }}">@icon('add-small')</button>
-                            </div>
-                        </li>
+                    class="scroll-box available-option-list">
+                    <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_available_operations_empty') }}</li>
+                    @foreach(\BookStack\Sorting\SortSetOperation::allExcluding($model?->getOperations() ?? []) as $operation)
+                        @include('settings.sort-sets.parts.operation', ['operation' => $operation])
                     @endforeach
                 </ul>
             </div>
diff --git a/resources/views/settings/sort-sets/parts/operation.blade.php b/resources/views/settings/sort-sets/parts/operation.blade.php
new file mode 100644
index 000000000..3feb68a47
--- /dev/null
+++ b/resources/views/settings/sort-sets/parts/operation.blade.php
@@ -0,0 +1,15 @@
+<li data-id="{{ $operation->value }}"
+    class="scroll-box-item items-center">
+    <div class="handle px-s">@icon('grip')</div>
+    <div class="text-small">{{ $operation->getLabel() }}</div>
+    <div class="buttons flex-container-row items-center ml-auto px-xxs py-xxs">
+        <button type="button" data-action="move_up" class="icon-button p-xxs"
+                title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
+        <button type="button" data-action="move_down" class="icon-button p-xxs"
+                title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
+        <button type="button" data-action="remove" class="icon-button p-xxs"
+                title="{{ trans('common.remove') }}">@icon('remove')</button>
+        <button type="button" data-action="add" class="icon-button p-xxs"
+                title="{{ trans('common.add') }}">@icon('add-small')</button>
+    </div>
+</li>
\ No newline at end of file
diff --git a/resources/views/shelves/parts/form.blade.php b/resources/views/shelves/parts/form.blade.php
index a75dd6ac1..7790ba5a4 100644
--- a/resources/views/shelves/parts/form.blade.php
+++ b/resources/views/shelves/parts/form.blade.php
@@ -38,7 +38,7 @@
         </div>
         <ul refs="shelf-sort@shelf-book-list"
             aria-labelledby="shelf-sort-books-label"
-            class="scroll-box">
+            class="scroll-box configured-option-list">
             @foreach (($shelf->visibleBooks ?? []) as $book)
                 @include('shelves.parts.shelf-sort-book-item', ['book' => $book])
             @endforeach
@@ -49,7 +49,7 @@
         <input type="text" refs="shelf-sort@book-search" class="scroll-box-search" placeholder="{{ trans('common.search') }}">
         <ul refs="shelf-sort@all-book-list"
             aria-labelledby="shelf-sort-all-books-label"
-            class="scroll-box">
+            class="scroll-box available-option-list">
             @foreach ($books as $book)
                 @include('shelves.parts.shelf-sort-book-item', ['book' => $book])
             @endforeach