diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php
index 537ea915b..d4642be78 100644
--- a/app/Http/Controllers/BookshelfController.php
+++ b/app/Http/Controllers/BookshelfController.php
@@ -64,7 +64,7 @@ class BookshelfController extends Controller
     public function create()
     {
         $this->checkPermission('bookshelf-create-all');
-        $books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
+        $books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
         $this->setPageTitle(trans('entities.shelves_create'));
 
         return view('shelves.create', ['books' => $books]);
@@ -140,7 +140,7 @@ class BookshelfController extends Controller
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
         $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
-        $books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug']);
+        $books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
 
         $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
 
diff --git a/resources/icons/add-small.svg b/resources/icons/add-small.svg
new file mode 100644
index 000000000..81aaf4f65
--- /dev/null
+++ b/resources/icons/add-small.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 13.114h-4.886V18h-2.228v-4.886H6v-2.228h4.886V6h2.228v4.886H18Z" style="stroke-width:.857143"/></svg>
\ No newline at end of file
diff --git a/resources/icons/remove.svg b/resources/icons/remove.svg
new file mode 100644
index 000000000..088c34a5d
--- /dev/null
+++ b/resources/icons/remove.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.5 7.91 16.09 6.5 12 10.59 7.91 6.5 6.5 7.91 10.59 12 6.5 16.09l1.41 1.41L12 13.41l4.09 4.09 1.41-1.41L13.41 12Z"/></svg>
\ No newline at end of file
diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js
index d10470bd7..e4aefc591 100644
--- a/resources/js/components/shelf-sort.js
+++ b/resources/js/components/shelf-sort.js
@@ -1,6 +1,30 @@
 import Sortable from "sortablejs";
 import {Component} from "./component";
 
+/**
+ * @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
+ */
+const itemActions = {
+    move_up(item, shelfBooksList, allBooksList) {
+        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, shelfBooksList, allBooksList) {
+        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, allBooksList) {
+        shelfBooksList.appendChild(item);
+    },
+};
+
 export class ShelfSort extends Component {
 
     setup() {
@@ -9,6 +33,9 @@ export class ShelfSort extends Component {
         this.shelfBookList = this.$refs.shelfBookList;
         this.allBookList = this.$refs.allBookList;
         this.bookSearchInput = this.$refs.bookSearch;
+        this.sortButtonContainer = this.$refs.sortButtonContainer;
+
+        this.lastSort = null;
 
         this.initSortable();
         this.setupListeners();
@@ -29,16 +56,22 @@ export class ShelfSort extends Component {
 
     setupListeners() {
         this.elem.addEventListener('click', event => {
-            const sortItem = event.target.closest('.scroll-box-item');
-            if (sortItem) {
-                event.preventDefault();
-                this.sortItemClick(sortItem);
+            const sortItemAction = event.target.closest('.scroll-box-item button[data-action]');
+            if (sortItemAction) {
+                this.sortItemActionClick(sortItemAction);
             }
         });
 
         this.bookSearchInput.addEventListener('input', event => {
             this.filterBooksByName(this.bookSearchInput.value);
         });
+
+        this.sortButtonContainer.addEventListener('click' , event => {
+            const button = event.target.closest('button[data-sort]');
+            if (button) {
+                this.sortShelfBooks(button.dataset.sort);
+            }
+        });
     }
 
     /**
@@ -62,15 +95,16 @@ export class ShelfSort extends Component {
     }
 
     /**
-     * Called when a sort item is clicked.
-     * @param {Element} sortItem
+     * Called when a sort item action button is clicked.
+     * @param {HTMLElement} sortItemAction
      */
-    sortItemClick(sortItem) {
-        const lists = this.elem.querySelectorAll('.scroll-box');
-        const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
-        if (newList.length > 0) {
-            newList[0].appendChild(sortItem);
-        }
+    sortItemActionClick(sortItemAction) {
+        const sortItem = sortItemAction.closest('.scroll-box-item');
+        const action = sortItemAction.dataset.action;
+
+        const actionFunction = itemActions[action];
+        actionFunction(sortItem, this.shelfBookList, this.allBookList);
+
         this.onChange();
     }
 
@@ -79,4 +113,27 @@ export class ShelfSort extends Component {
         this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
     }
 
+    sortShelfBooks(sortProperty) {
+        const books = Array.from(this.shelfBookList.children);
+        const reverse = sortProperty === this.lastSort;
+
+        books.sort((bookA, bookB) => {
+            const aProp = bookA.dataset[sortProperty].toLowerCase();
+            const bProp = bookB.dataset[sortProperty].toLowerCase();
+
+            if (reverse) {
+                return aProp < bProp ? (aProp === bProp ? 0 : 1) : -1;
+            }
+
+            return aProp < bProp ? (aProp === bProp ? 0 : -1) : 1;
+        });
+
+        for (const book of books) {
+            this.shelfBookList.append(book);
+        }
+
+        this.lastSort = (this.lastSort === sortProperty) ? null : sortProperty;
+        this.onChange();
+    }
+
 }
\ No newline at end of file
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 2150f6d07..825501364 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -1050,4 +1050,89 @@ $btt-size: 40px;
     vertical-align: top;
     line-height: 2;
   }
+}
+
+// Sortable scroll boxes
+.scroll-box {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  max-height: 280px;
+  overflow-y: scroll;
+  border: 1px solid;
+  @include lightDark(border-color, #DDD, #000);
+  border-radius: 3px;
+  min-height: 20px;
+  @include lightDark(background-color, #EEE, #000);
+}
+.scroll-box-item {
+  border-bottom: 1px solid;
+  border-top: 1px solid;
+  @include lightDark(border-color, #DDD, #000);
+  margin-top: -1px;
+  @include lightDark(background-color, #FFF, #222);
+  display: flex;
+  align-items: flex-start;
+  padding: 1px;
+  &:last-child {
+    border-bottom: 0;
+  }
+  &:hover {
+    cursor: pointer;
+    @include lightDark(background-color, #f8f8f8, #333);
+  }
+  .handle {
+    color: #AAA;
+    cursor: grab;
+  }
+  button {
+    opacity: .6;
+  }
+  .handle svg {
+    margin: 0;
+  }
+  > * {
+    padding: $-xs $-m;
+  }
+  .handle + * {
+    padding-left: 0;
+  }
+  &:hover .handle {
+    @include lightDark(color, #444, #FFF);
+  }
+  &:hover button {
+    opacity: 1;
+  }
+  a:hover {
+    text-decoration: none;
+  }
+}
+
+input.scroll-box-search, .scroll-box-header-item {
+  font-size: 0.8rem;
+  border: 1px solid;
+  @include lightDark(border-color, #DDD, #000);
+  @include lightDark(background-color, #FFF, #222);
+  margin-bottom: -1px;
+  border-radius: 3px 3px 0 0;
+  width: 100%;
+  max-width: 100%;
+  height: auto;
+  line-height: 1.4;
+  color: #666;
+}
+
+.scroll-box-search + .scroll-box,
+.scroll-box-header-item + .scroll-box {
+  border-radius: 0 0 3px 3px;
+}
+
+.scroll-box[refs="shelf-sort@shelf-book-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"],
+{
+  display: none;
 }
\ No newline at end of file
diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss
index 668cb5c85..0f4ec7041 100644
--- a/resources/sass/styles.scss
+++ b/resources/sass/styles.scss
@@ -198,71 +198,6 @@ $loadingSize: 10px;
   }
 }
 
-.scroll-box {
-  max-height: 250px;
-  overflow-y: scroll;
-  border: 1px solid;
-  @include lightDark(border-color, #DDD, #000);
-  border-radius: 3px;
-  min-height: 20px;
-  @include lightDark(background-color, #EEE, #000);
-}
-.scroll-box-item {
-  border-bottom: 1px solid;
-  border-top: 1px solid;
-  @include lightDark(border-color, #DDD, #000);
-  margin-top: -1px;
-  @include lightDark(background-color, #FFF, #222);
-  display: flex;
-  padding: 1px;
-  &:last-child {
-    border-bottom: 0;
-  }
-  &:hover {
-    cursor: pointer;
-    @include lightDark(background-color, #f8f8f8, #333);
-  }
-  .handle {
-    color: #AAA;
-    cursor: grab;
-  }
-  .handle svg {
-    margin: 0;
-  }
-  > * {
-    padding: $-xs $-m;
-  }
-  .handle + * {
-    padding-left: 0;
-  }
-  &:hover .handle {
-    @include lightDark(color, #444, #FFF);
-  }
-  a:hover {
-    text-decoration: none;
-  }
-}
-
-input.scroll-box-search, .scroll-box-header-item {
-  font-size: 0.8rem;
-  padding: $-xs $-m;
-  border: 1px solid;
-  @include lightDark(border-color, #DDD, #000);
-  @include lightDark(background-color, #FFF, #222);
-  margin-bottom: -1px;
-  border-radius: 3px 3px 0 0;
-  width: 100%;
-  max-width: 100%;
-  height: auto;
-  line-height: 1.4;
-  color: #666;
-}
-
-.scroll-box-search + .scroll-box,
-.scroll-box-header-item + .scroll-box {
-  border-radius: 0 0 3px 3px;
-}
-
 .fullscreen {
   border:0;
   position:fixed;
diff --git a/resources/views/shelves/parts/form.blade.php b/resources/views/shelves/parts/form.blade.php
index 364f8e0be..ad67cb85c 100644
--- a/resources/views/shelves/parts/form.blade.php
+++ b/resources/views/shelves/parts/form.blade.php
@@ -12,32 +12,45 @@
 
 <div component="shelf-sort" class="grid half gap-xl">
     <div class="form-group">
-        <label for="books">{{ trans('entities.shelves_books') }}</label>
+        <label for="books" id="shelf-sort-books-label">{{ trans('entities.shelves_books') }}</label>
         <input refs="shelf-sort@input" type="hidden" name="books"
                value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
-        <div class="scroll-box-header-item">{{ trans('entities.shelves_drag_books') }}</div>
-        <div refs="shelf-sort@shelf-book-list" class="scroll-box">
-            @if (count($shelf->visibleBooks ?? []) > 0)
-                @foreach ($shelf->visibleBooks as $book)
-                    <div data-id="{{ $book->id }}" class="scroll-box-item">
-                        <div class="handle">@icon('grip')</div>
-                        <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
-                    </div>
-                @endforeach
-            @endif
+        <div class="scroll-box-header-item flex-container-row items-center py-xs">
+            <span class="px-m py-xs">{{ trans('entities.shelves_drag_books') }}</span>
+            <div class="dropdown-container ml-auto" component="dropdown">
+                <button refs="dropdown@toggle"
+                        type="button"
+                        title="{{ trans('common.more') }}"
+                        class="icon-button px-xs py-xxs mx-xs text-bigger"
+                        aria-haspopup="true"
+                        aria-expanded="false">
+                    @icon('more')
+                </button>
+                <div refs="dropdown@menu shelf-sort@sort-button-container" class="dropdown-menu" role="menu">
+                    <button type="button" class="text-item" data-sort="name">{{ trans('entities.books_sort_name') }}</button>
+                    <button type="button" class="text-item" data-sort="created">{{ trans('entities.books_sort_created') }}</button>
+                    <button type="button" class="text-item" data-sort="updated">{{ trans('entities.books_sort_updated') }}</button>
+                </div>
+            </div>
         </div>
+        <ul refs="shelf-sort@shelf-book-list"
+            aria-labelledby="shelf-sort-books-label"
+            class="scroll-box">
+            @foreach (($shelf->visibleBooks ?? []) as $book)
+                @include('shelves.parts.shelf-sort-book-item', ['book' => $book])
+            @endforeach
+        </ul>
     </div>
     <div class="form-group">
-        <label for="books">{{ trans('entities.shelves_add_books') }}</label>
+        <label for="books" id="shelf-sort-all-books-label">{{ trans('entities.shelves_add_books') }}</label>
         <input type="text" refs="shelf-sort@book-search" class="scroll-box-search" placeholder="{{ trans('common.search') }}">
-        <div refs="shelf-sort@all-book-list" class="scroll-box">
+        <ul refs="shelf-sort@all-book-list"
+            aria-labelledby="shelf-sort-all-books-label"
+            class="scroll-box">
             @foreach ($books as $book)
-                <div data-id="{{ $book->id }}" class="scroll-box-item">
-                    <div class="handle">@icon('grip')</div>
-                    <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
-                </div>
+                @include('shelves.parts.shelf-sort-book-item', ['book' => $book])
             @endforeach
-        </div>
+        </ul>
     </div>
 </div>
 
diff --git a/resources/views/shelves/parts/shelf-sort-book-item.blade.php b/resources/views/shelves/parts/shelf-sort-book-item.blade.php
new file mode 100644
index 000000000..795aee7be
--- /dev/null
+++ b/resources/views/shelves/parts/shelf-sort-book-item.blade.php
@@ -0,0 +1,18 @@
+<li data-id="{{ $book->id }}"
+     data-name="{{ $book->name }}"
+     data-created="{{ $book->created_at->timestamp }}"
+     data-updated="{{ $book->updated_at->timestamp }}"
+     class="scroll-box-item">
+    <div class="handle px-s">@icon('grip')</div>
+    <div class="text-book">@icon('book'){{ $book->name }}</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>
\ No newline at end of file