diff --git a/resources/js/components/book-sort.js b/resources/js/components/book-sort.js
index 3ffadf991..5ae283fd0 100644
--- a/resources/js/components/book-sort.js
+++ b/resources/js/components/book-sort.js
@@ -1,4 +1,4 @@
-import Sortable from "sortablejs";
+import Sortable, {MultiDrag} from "sortablejs";
 import {Component} from "./component";
 import {htmlToDom} from "../services/dom";
 
@@ -37,6 +37,113 @@ const sortOperations = {
     },
 };
 
+/**
+ * The available move actions.
+ * The active function indicates if the action is possible for the given item.
+ * The run function performs the move.
+ * @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}}
+ */
+const moveActions = {
+    up: {
+        active(elem, parent, book) {
+            return !(elem.previousElementSibling === null && !parent);
+        },
+        run(elem, parent, book) {
+            const newSibling = elem.previousElementSibling || parent;
+            newSibling.insertAdjacentElement('beforebegin', elem);
+        }
+    },
+    down: {
+        active(elem, parent, book) {
+            return !(elem.nextElementSibling === null && !parent);
+        },
+        run(elem, parent, book) {
+            const newSibling = elem.nextElementSibling || parent;
+            newSibling.insertAdjacentElement('afterend', elem);
+        }
+    },
+    next_book: {
+        active(elem, parent, book) {
+            return book.nextElementSibling !== null;
+        },
+        run(elem, parent, book) {
+            const newList = book.nextElementSibling.querySelector('ul');
+            newList.prepend(elem);
+        }
+    },
+    prev_book: {
+        active(elem, parent, book) {
+            return book.previousElementSibling !== null;
+        },
+        run(elem, parent, book) {
+            const newList = book.previousElementSibling.querySelector('ul');
+            newList.appendChild(elem);
+        }
+    },
+    next_chapter: {
+        active(elem, parent, book) {
+            return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
+        },
+        run(elem, parent, book) {
+            const nextChapter = this.getNextChapter(elem, parent);
+            nextChapter.querySelector('ul').prepend(elem);
+        },
+        getNextChapter(elem, parent) {
+            const topLevel = (parent || elem);
+            const topItems = Array.from(topLevel.parentElement.children);
+            const index = topItems.indexOf(topLevel);
+            return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
+        }
+    },
+    prev_chapter: {
+        active(elem, parent, book) {
+            return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
+        },
+        run(elem, parent, book) {
+            const prevChapter = this.getPrevChapter(elem, parent);
+            prevChapter.querySelector('ul').append(elem);
+        },
+        getPrevChapter(elem, parent) {
+            const topLevel = (parent || elem);
+            const topItems = Array.from(topLevel.parentElement.children);
+            const index = topItems.indexOf(topLevel);
+            return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
+        }
+    },
+    book_end: {
+        active(elem, parent, book) {
+            return parent || (parent === null && elem.nextElementSibling);
+        },
+        run(elem, parent, book) {
+            book.querySelector('ul').append(elem);
+        }
+    },
+    book_start: {
+        active(elem, parent, book) {
+            return parent || (parent === null && elem.previousElementSibling);
+        },
+        run(elem, parent, book) {
+            book.querySelector('ul').prepend(elem);
+        }
+    },
+    before_chapter: {
+        active(elem, parent, book) {
+            return parent;
+        },
+        run(elem, parent, book) {
+            parent.insertAdjacentElement('beforebegin', elem);
+        }
+    },
+    after_chapter: {
+        active(elem, parent, book) {
+            return parent;
+        },
+        run(elem, parent, book) {
+            parent.insertAdjacentElement('afterend', elem);
+        }
+    },
+};
+
 export class BookSort extends Component {
 
     setup() {
@@ -44,15 +151,34 @@ export class BookSort extends Component {
         this.sortContainer = this.$refs.sortContainer;
         this.input = this.$refs.input;
 
+        Sortable.mount(new MultiDrag());
+
         const initialSortBox = this.container.querySelector('.sort-box');
         this.setupBookSortable(initialSortBox);
         this.setupSortPresets();
+        this.setupMoveActions();
 
-        window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
+        window.$events.listen('entity-select-change', this.bookSelect.bind(this));
     }
 
     /**
-     * Setup the handlers for the preset sort type buttons.
+     * Set up the handlers for the item-level move buttons.
+     */
+    setupMoveActions() {
+        // Handle move button click
+        this.container.addEventListener('click', event => {
+            if (event.target.matches('[data-move]')) {
+                const action = event.target.getAttribute('data-move');
+                const sortItem = event.target.closest('[data-id]');
+                this.runSortAction(sortItem, action);
+            }
+        });
+
+        this.updateMoveActionStateForAll();
+    }
+
+    /**
+     * Set up the handlers for the preset sort type buttons.
      */
     setupSortPresets() {
         let lastSort = '';
@@ -100,16 +226,19 @@ export class BookSort extends Component {
             const newBookContainer = htmlToDom(resp.data);
             this.sortContainer.append(newBookContainer);
             this.setupBookSortable(newBookContainer);
+            this.updateMoveActionStateForAll();
+
+            const summary = newBookContainer.querySelector('summary');
+            summary.focus();
         });
     }
 
     /**
-     * Setup the given book container element to have sortable items.
+     * Set up the given book container element to have sortable items.
      * @param {Element} bookContainer
      */
     setupBookSortable(bookContainer) {
-        const sortElems = [bookContainer.querySelector('.sort-list')];
-        sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
+        const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist'));
 
         const bookGroupConfig = {
             name: 'book',
@@ -125,22 +254,40 @@ export class BookSort extends Component {
             }
         };
 
-        for (let sortElem of sortElems) {
-            new Sortable(sortElem, {
+        for (const sortElem of sortElems) {
+            Sortable.create(sortElem, {
                 group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
                 animation: 150,
                 fallbackOnBody: true,
                 swapThreshold: 0.65,
-                onSort: this.updateMapInput.bind(this),
+                onSort: (event) => {
+                    this.ensureNoNestedChapters()
+                    this.updateMapInput();
+                    this.updateMoveActionStateForAll();
+                },
                 dragClass: 'bg-white',
                 ghostClass: 'primary-background-light',
                 multiDrag: true,
-                multiDragKey: 'CTRL',
+                multiDragKey: 'Control',
                 selectedClass: 'sortable-selected',
             });
         }
     }
 
+    /**
+     * Handle nested chapters by moving them to the parent book.
+     * Needed since sorting with multi-sort only checks group rules based on the active item,
+     * not all in group, therefore need to manually check after a sort.
+     * Must be done before updating the map input.
+     */
+    ensureNoNestedChapters() {
+        const nestedChapters = this.container.querySelectorAll('[data-type="chapter"] [data-type="chapter"]');
+        for (const chapter of nestedChapters) {
+            const parentChapter = chapter.parentElement.closest('[data-type="chapter"]');
+            parentChapter.insertAdjacentElement('afterend', chapter);
+        }
+    }
+
     /**
      * Update the input with our sort data.
      */
@@ -202,4 +349,38 @@ export class BookSort extends Component {
         }
     }
 
+    /**
+     * Run the given sort action up the provided sort item.
+     * @param {Element} item
+     * @param {String} action
+     */
+    runSortAction(item, action) {
+        const parentItem = item.parentElement.closest('li[data-id]');
+        const parentBook = item.parentElement.closest('[data-type="book"]');
+        moveActions[action].run(item, parentItem, parentBook);
+        this.updateMapInput();
+        this.updateMoveActionStateForAll();
+        item.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+        item.focus();
+    }
+
+    /**
+     * Update the state of the available move actions on this item.
+     * @param {Element} item
+     */
+    updateMoveActionState(item) {
+        const parentItem = item.parentElement.closest('li[data-id]');
+        const parentBook = item.parentElement.closest('[data-type="book"]');
+        for (const [action, functions] of Object.entries(moveActions)) {
+            const moveButton = item.querySelector(`[data-move="${action}"]`);
+            moveButton.disabled = !functions.active(item, parentItem, parentBook);
+        }
+    }
+
+    updateMoveActionStateForAll() {
+        const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]');
+        for (const item of items) {
+            this.updateMoveActionState(item);
+        }
+    }
 }
\ No newline at end of file
diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js
index 1384b33a9..09d14b233 100644
--- a/resources/js/components/entity-selector.js
+++ b/resources/js/components/entity-selector.js
@@ -15,7 +15,6 @@ export class EntitySelector extends Component {
         this.searchInput = this.$refs.search;
         this.loading = this.$refs.loading;
         this.resultsContainer = this.$refs.results;
-        this.addButton = this.$refs.add;
 
         this.search = '';
         this.lastClick = 0;
@@ -43,15 +42,6 @@ export class EntitySelector extends Component {
             if (event.keyCode === 13) event.preventDefault();
         });
 
-        if (this.addButton) {
-            this.addButton.addEventListener('click', event => {
-                if (this.selectedItemData) {
-                    this.confirmSelection(this.selectedItemData);
-                    this.unselectAll();
-                }
-            });
-        }
-
         // Keyboard navigation
         onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
             if (e.ctrlKey && e.code === 'Enter') {
diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.js
index 0e1dcf1a7..0f866ceaa 100644
--- a/resources/js/services/keyboard-navigation.js
+++ b/resources/js/services/keyboard-navigation.js
@@ -86,7 +86,7 @@ export class KeyboardNavigationHandler {
      */
     #getFocusable() {
         const focusable = [];
-        const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])';
+        const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
         for (const container of this.containers) {
             focusable.push(...container.querySelectorAll(selector))
         }
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index fa2586f8d..8bf805774 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -141,6 +141,7 @@ return [
     'books_search_this' => 'Search this book',
     'books_navigation' => 'Book Navigation',
     'books_sort' => 'Sort Book Contents',
+    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.',
     'books_sort_named' => 'Sort Book :bookName',
     'books_sort_name' => 'Sort by Name',
     'books_sort_created' => 'Sort by Created Date',
@@ -149,6 +150,17 @@ return [
     'books_sort_chapters_last' => 'Chapters Last',
     'books_sort_show_other' => 'Show Other Books',
     'books_sort_save' => 'Save New Order',
+    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
+    'books_sort_move_up' => 'Move Up',
+    'books_sort_move_down' => 'Move Down',
+    'books_sort_move_prev_book' => 'Move to Previous Book',
+    'books_sort_move_next_book' => 'Move to Next Book',
+    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
+    'books_sort_move_next_chapter' => 'Move Into Next Chapter',
+    'books_sort_move_book_start' => 'Move to Start of Book',
+    'books_sort_move_book_end' => 'Move to End of Book',
+    'books_sort_move_before_chapter' => 'Move to Before Chapter',
+    'books_sort_move_after_chapter' => 'Move to After Chapter',
     'books_copy' => 'Copy Book',
     'books_copy_success' => 'Book successfully copied',
 
diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss
index 4c7de600b..3fc419046 100644
--- a/resources/sass/_layout.scss
+++ b/resources/sass/_layout.scss
@@ -268,6 +268,11 @@ body.flexbox {
   }
 }
 
+.sticky-top-m {
+  position: sticky;
+  top: $-m;
+}
+
 /**
  * Visibility
  */
diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss
index 86a89051f..33e500d6a 100644
--- a/resources/sass/_lists.scss
+++ b/resources/sass/_lists.scss
@@ -232,7 +232,7 @@
 }
 
 // Sortable Lists
-.sortable-page-list, .sortable-page-list ul {
+.sortable-page-list, .sortable-page-sublist {
   list-style: none;
 }
 .sort-box {
@@ -267,7 +267,7 @@
   .entity-list-item > span:first-child {
     align-self: flex-start;
   }
-  .sortable-selected  .entity-list-item, .sortable-selected  .entity-list-item:hover {
+  .sortable-selected, .sortable-selected:hover {
     outline: 1px dotted var(--color-primary);
     background-color: var(--color-primary-light) !important;
   }
@@ -278,12 +278,13 @@
   > ul {
     margin-inline-start: 0;
   }
-  ul {
+  .sortable-page-sublist {
     margin-bottom: $-m;
     margin-top: 0;
     padding-inline-start: $-m;
   }
   li {
+    @include lightDark(background-color, #FFF, #222);
     border: 1px solid;
     @include lightDark(border-color, #DDD, #666);
     margin-top: -1px;
@@ -302,6 +303,36 @@
 .sortable-page-list li.placeholder:before {
   position: absolute;
 }
+.sort-box summary {
+  list-style: none;
+  font-size: .9rem;
+  cursor: pointer;
+}
+.sort-box summary::-webkit-details-marker {
+  display: none;
+}
+details.sort-box summary .caret-container svg {
+  transition: transform ease-in-out 120ms;
+}
+details.sort-box[open] summary .caret-container svg {
+  transform: rotate(90deg);
+}
+.sort-box-actions .icon-button {
+  opacity: .6;
+}
+.sort-box .flex-container-row:hover .sort-box-actions .icon-button,
+.sort-box .flex-container-row:focus-within .sort-box-actions .icon-button {
+  opacity: 1;
+}
+.sort-box-actions .icon-button[disabled] {
+  visibility: hidden;
+}
+.sort-box-actions .dropdown-menu button[disabled] {
+  display: none;
+}
+.sort-list-handle {
+  cursor: grab;
+}
 
 .activity-list-item {
   padding: $-s 0;
diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss
index 23959d1f8..e50a2f96a 100644
--- a/resources/sass/styles.scss
+++ b/resources/sass/styles.scss
@@ -187,18 +187,14 @@ $loadingSize: 10px;
     height: 400px;
     padding-top: $-l;
   }
-  .entity-selector-add button {
-    margin: 0;
-    display: block;
-    width: 100%;
-    border: 0;
-    border-top: 1px solid #DDD;
-  }
   &.compact {
     font-size: 10px;
     .entity-item-snippet {
       display: none;
     }
+    h4 {
+      font-size: 14px;
+    }
   }
 }
 
diff --git a/resources/views/books/parts/sort-box-actions.blade.php b/resources/views/books/parts/sort-box-actions.blade.php
new file mode 100644
index 000000000..3796ffafb
--- /dev/null
+++ b/resources/views/books/parts/sort-box-actions.blade.php
@@ -0,0 +1,26 @@
+<div class="sort-box-actions flex-container-row items-center px-s gap-xxs">
+    <button type="button" data-move="up" class="icon-button p-xs text-bigger"
+            title="{{ trans('entities.books_sort_move_up') }}">@icon('chevron-up')</button>
+    <button type="button" data-move="down" class="icon-button p-xs text-bigger"
+            title="{{ trans('entities.books_sort_move_down') }}">@icon('chevron-down')</button>
+    <div class="dropdown-container" component="dropdown">
+        <button refs="dropdown@toggle"
+                type="button"
+                title="{{ trans('common.more') }}"
+                class="icon-button p-xs text-bigger"
+                aria-haspopup="true"
+                aria-expanded="false">
+            @icon('more')
+        </button>
+        <div refs="dropdown@menu" class="dropdown-menu" role="menu">
+            <button type="button" class="text-item" data-move="prev_book">{{ trans('entities.books_sort_move_prev_book') }}</button>
+            <button type="button" class="text-item" data-move="next_book">{{ trans('entities.books_sort_move_next_book') }}</button>
+            <button type="button" class="text-item" data-move="prev_chapter">{{ trans('entities.books_sort_move_prev_chapter') }}</button>
+            <button type="button" class="text-item" data-move="next_chapter">{{ trans('entities.books_sort_move_next_chapter') }}</button>
+            <button type="button" class="text-item" data-move="book_start">{{ trans('entities.books_sort_move_book_start') }}</button>
+            <button type="button" class="text-item" data-move="book_end">{{ trans('entities.books_sort_move_book_end') }}</button>
+            <button type="button" class="text-item" data-move="before_chapter">{{ trans('entities.books_sort_move_before_chapter') }}</button>
+            <button type="button" class="text-item" data-move="after_chapter">{{ trans('entities.books_sort_move_after_chapter') }}</button>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/books/parts/sort-box.blade.php b/resources/views/books/parts/sort-box.blade.php
index ef9929e46..03998e261 100644
--- a/resources/views/books/parts/sort-box.blade.php
+++ b/resources/views/books/parts/sort-box.blade.php
@@ -1,8 +1,15 @@
-<div class="sort-box" data-type="book" data-id="{{ $book->id }}">
-    <h5 class="text-book entity-list-item no-hover py-xs pl-none">
-        <span>@icon('book')</span>
-        <span>{{ $book->name }}</span>
-    </h5>
+<details class="sort-box" data-type="book" data-id="{{ $book->id }}" open>
+    <summary>
+        <h5 class="flex-container-row items-center justify-flex-start gap-xs">
+            <div class="text-book text-bigger caret-container">
+                @icon('caret-right')
+            </div>
+            <div class="entity-list-item no-hover py-s text-book px-none">
+                <span>@icon('book')</span>
+                <span>{{ $book->name }}</span>
+            </div>
+        </h5>
+    </summary>
     <div class="sort-box-options pb-sm">
         <button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
         <button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
@@ -14,29 +21,39 @@
 
         @foreach($bookChildren as $bookChild)
             <li class="text-{{ $bookChild->getType() }}"
-                data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}"
-                data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}"
-                data-updated="{{ $bookChild->updated_at->timestamp }}">
-                <div class="entity-list-item">
-                    <span>@icon($bookChild->getType()) </span>
-                    <div>
-                        {{ $bookChild->name }}
+                data-id="{{$bookChild->id}}"
+                data-type="{{ $bookChild->getType() }}"
+                data-name="{{ $bookChild->name }}"
+                data-created="{{ $bookChild->created_at->timestamp }}"
+                data-updated="{{ $bookChild->updated_at->timestamp }}"
+                tabindex="-1">
+                <div class="flex-container-row items-center">
+                    <div class="text-muted sort-list-handle px-s py-m">@icon('grip')</div>
+                    <div class="entity-list-item px-none no-hover">
+                        <span>@icon($bookChild->getType()) </span>
                         <div>
+                            {{ $bookChild->name }}
+                            <div>
 
+                            </div>
                         </div>
                     </div>
+                    @include('books.parts.sort-box-actions')
                 </div>
                 @if($bookChild->isA('chapter'))
-                    <ul>
+                    <ul class="sortable-page-sublist">
                         @foreach($bookChild->visible_pages as $page)
-                            <li class="text-page"
+                            <li class="text-page flex-container-row items-center"
                                 data-id="{{$page->id}}" data-type="page"
                                 data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
-                                data-updated="{{ $page->updated_at->timestamp }}">
-                                <div class="entity-list-item">
+                                data-updated="{{ $page->updated_at->timestamp }}"
+                                tabindex="-1">
+                                <div class="text-muted sort-list-handle px-s py-m">@icon('grip')</div>
+                                <div class="entity-list-item px-none no-hover">
                                     <span>@icon('page')</span>
                                     <span>{{ $page->name }}</span>
                                 </div>
+                                @include('books.parts.sort-box-actions')
                             </li>
                         @endforeach
                     </ul>
@@ -45,4 +62,4 @@
         @endforeach
 
     </ul>
-</div>
\ No newline at end of file
+</details>
\ No newline at end of file
diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php
index 077da101d..c82ad4e3b 100644
--- a/resources/views/books/sort.blade.php
+++ b/resources/views/books/sort.blade.php
@@ -16,8 +16,10 @@
 
         <div class="grid left-focus gap-xl">
             <div>
-                <div component="book-sort" class="card content-wrap">
-                    <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
+                <div component="book-sort" class="card content-wrap auto-height">
+                    <h1 class="list-heading">{{ trans('entities.books_sort') }}</h1>
+                    <p class="text-muted">{{ trans('entities.books_sort_desc') }}</p>
+
                     <div refs="book-sort@sortContainer">
                         @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
                     </div>
@@ -35,10 +37,11 @@
             </div>
 
             <div>
-                <main class="card content-wrap">
-                    <h2 class="list-heading mb-m">{{ trans('entities.books_sort_show_other') }}</h2>
+                <main class="card content-wrap auto-height sticky-top-m">
+                    <h2 class="list-heading">{{ trans('entities.books_sort_show_other') }}</h2>
+                    <p class="text-muted">{{ trans('entities.books_sort_show_other_desc') }}</p>
 
-                    @include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true])
+                    @include('entities.selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update'])
 
                 </main>
             </div>
diff --git a/resources/views/entities/selector.blade.php b/resources/views/entities/selector.blade.php
index 45fae4707..a9f5b932c 100644
--- a/resources/views/entities/selector.blade.php
+++ b/resources/views/entities/selector.blade.php
@@ -8,11 +8,5 @@
         <input refs="entity-selector@search" type="text" placeholder="{{ trans('common.search') }}" @if($autofocus ?? false) autofocus @endif>
         <div class="text-center loading" refs="entity-selector@loading">@include('common.loading-icon')</div>
         <div refs="entity-selector@results"></div>
-        @if($showAdd ?? false)
-            <div class="entity-selector-add">
-                <button refs="entity-selector@add" type="button"
-                        class="button outline">@icon('add'){{ trans('common.add') }}</button>
-            </div>
-        @endif
     </div>
 </div>
\ No newline at end of file