diff --git a/client/components/ui/ContextMenuDropdown.vue b/client/components/ui/ContextMenuDropdown.vue
new file mode 100644
index 00000000..988f7469
--- /dev/null
+++ b/client/components/ui/ContextMenuDropdown.vue
@@ -0,0 +1,55 @@
+<template>
+  <div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
+    <button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
+      <span class="material-icons">more_vert</span>
+    </button>
+
+    <transition name="menu">
+      <div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
+        <template v-for="(item, index) in items">
+          <div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
+            <p>{{ item.text }}</p>
+          </div>
+        </template>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    disabled: Boolean,
+    items: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      clickOutsideObj: {
+        handler: this.clickedOutside,
+        events: ['mousedown'],
+        isActive: true
+      },
+      showMenu: false
+    }
+  },
+  computed: {},
+  methods: {
+    clickShowMenu() {
+      if (this.disabled) return
+      this.showMenu = !this.showMenu
+    },
+    clickedOutside() {
+      this.showMenu = false
+    },
+    clickAction(action) {
+      if (this.disabled) return
+      this.showMenu = false
+      this.$emit('action', action)
+    }
+  },
+  mounted() {}
+}
+</script>
\ No newline at end of file
diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue
index 70b446bd..0aa313ea 100644
--- a/client/pages/collection/_id.vue
+++ b/client/pages/collection/_id.vue
@@ -19,9 +19,11 @@
               {{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
             </ui-btn>
 
-            <ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
+            <button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
+              <span class="material-icons text-xl">edit</span>
+            </button>
 
-            <ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
+            <ui-context-menu-dropdown :items="contextMenuItems" class="mx-px" @action="contextMenuAction" />
           </div>
 
           <div class="my-8 max-w-2xl">
@@ -32,7 +34,7 @@
         </div>
       </div>
     </div>
-    <div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
+    <div v-show="processing" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
       <ui-loading-indicator />
     </div>
   </div>
@@ -64,7 +66,7 @@ export default {
   },
   data() {
     return {
-      processingRemove: false
+      processing: false
     }
   },
   computed: {
@@ -102,15 +104,55 @@ export default {
     },
     userCanDelete() {
       return this.$store.getters['user/getUserCanDelete']
+    },
+    contextMenuItems() {
+      const items = [
+        {
+          text: 'Create playlist from collection',
+          action: 'create-playlist'
+        }
+      ]
+      if (this.userCanDelete) {
+        items.push({
+          text: 'Delete collection',
+          action: 'delete'
+        })
+      }
+      return items
     }
   },
   methods: {
+    contextMenuAction(action) {
+      if (action === 'delete') {
+        this.removeClick()
+      } else if (action === 'create-playlist') {
+        this.createPlaylistFromCollection()
+      }
+    },
+    createPlaylistFromCollection() {
+      this.processing = true
+      this.$axios
+        .$post(`/api/playlists/collection/${this.collectionId}`)
+        .then((playlist) => {
+          if (playlist) {
+            this.$toast.success('Playlist created')
+            this.$router.push(`/playlist/${playlist.id}`)
+          }
+        })
+        .catch((error) => {
+          const errMsg = error.response ? error.response.data || '' : ''
+          this.$toast.error(errMsg || 'Failed to create playlist')
+        })
+        .finally(() => {
+          this.processing = false
+        })
+    },
     editClick() {
       this.$store.commit('globals/setEditCollection', this.collection)
     },
     removeClick() {
       if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
-        this.processingRemove = true
+        this.processing = true
         this.$axios
           .$delete(`/api/collections/${this.collection.id}`)
           .then(() => {
@@ -121,7 +163,7 @@ export default {
             this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
           })
           .finally(() => {
-            this.processingRemove = false
+            this.processing = false
           })
       }
     },
diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js
index a00ae867..f4161f37 100644
--- a/server/controllers/CollectionController.js
+++ b/server/controllers/CollectionController.js
@@ -123,7 +123,7 @@ class CollectionController {
 
   middleware(req, res, next) {
     if (req.params.id) {
-      var collection = this.db.collections.find(c => c.id === req.params.id)
+      const collection = this.db.collections.find(c => c.id === req.params.id)
       if (!collection) {
         return res.status(404).send('Collection not found')
       }
diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js
index ecabb686..e923cf82 100644
--- a/server/controllers/PlaylistController.js
+++ b/server/controllers/PlaylistController.js
@@ -174,9 +174,42 @@ class PlaylistController {
     res.json(jsonExpanded)
   }
 
+  // POST: api/playlists/collection/:collectionId
+  async createFromCollection(req, res) {
+    let collection = this.db.collections.find(c => c.id === req.params.collectionId)
+    if (!collection) {
+      return res.status(404).send('Collection not found')
+    }
+    // Expand collection to get library items
+    collection = collection.toJSONExpanded(this.db.libraryItems)
+
+    // Filter out library items not accessible to user
+    const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
+
+    if (!libraryItems.length) {
+      return res.status(400).send('Collection has no books accessible to user')
+    }
+
+    const newPlaylist = new Playlist()
+
+    const newPlaylistData = {
+      userId: req.user.id,
+      libraryId: collection.libraryId,
+      name: collection.name,
+      description: collection.description || null,
+      items: libraryItems.map(li => ({ libraryItemId: li.id }))
+    }
+    newPlaylist.setData(newPlaylistData)
+
+    const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
+    await this.db.insertEntity('playlist', newPlaylist)
+    SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
+    res.json(jsonExpanded)
+  }
+
   middleware(req, res, next) {
     if (req.params.id) {
-      var playlist = this.db.playlists.find(p => p.id === req.params.id)
+      const playlist = this.db.playlists.find(p => p.id === req.params.id)
       if (!playlist) {
         return res.status(404).send('Playlist not found')
       }
diff --git a/server/objects/Collection.js b/server/objects/Collection.js
index cab8348d..813ced21 100644
--- a/server/objects/Collection.js
+++ b/server/objects/Collection.js
@@ -38,10 +38,10 @@ class Collection {
   }
 
   toJSONExpanded(libraryItems, minifiedBooks = false) {
-    var json = this.toJSON()
+    const json = this.toJSON()
     json.books = json.books.map(bookId => {
-      var _ab = libraryItems.find(li => li.id === bookId)
-      return _ab ? minifiedBooks ? _ab.toJSONMinified() : _ab.toJSONExpanded() : null
+      const book = libraryItems.find(li => li.id === bookId)
+      return book ? minifiedBooks ? book.toJSONMinified() : book.toJSONExpanded() : null
     }).filter(b => !!b)
     return json
   }
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index b219763a..88129244 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -143,7 +143,7 @@ class ApiRouter {
     //
     // Playlist Routes
     //
-    this.router.post('/playlists', PlaylistController.middleware.bind(this), PlaylistController.create.bind(this))
+    this.router.post('/playlists', PlaylistController.create.bind(this))
     this.router.get('/playlists', PlaylistController.findAllForUser.bind(this))
     this.router.get('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.findOne.bind(this))
     this.router.patch('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.update.bind(this))
@@ -152,6 +152,7 @@ class ApiRouter {
     this.router.delete('/playlists/:id/item/:libraryItemId/:episodeId?', PlaylistController.middleware.bind(this), PlaylistController.removeItem.bind(this))
     this.router.post('/playlists/:id/batch/add', PlaylistController.middleware.bind(this), PlaylistController.addBatch.bind(this))
     this.router.post('/playlists/:id/batch/remove', PlaylistController.middleware.bind(this), PlaylistController.removeBatch.bind(this))
+    this.router.post('/playlists/collection/:collectionId', PlaylistController.createFromCollection.bind(this))
 
     //
     // Current User Routes (Me)