diff --git a/package-lock.json b/package-lock.json
index f234d4f9d..352ad5ce9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -441,22 +441,6 @@
       "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
       "dev": true
     },
-    "axios": {
-      "version": "0.19.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
-      "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
-      "requires": {
-        "follow-redirects": "1.5.10",
-        "is-buffer": "^2.0.2"
-      },
-      "dependencies": {
-        "is-buffer": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
-          "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
-        }
-      }
-    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -1855,24 +1839,6 @@
         }
       }
     },
-    "follow-redirects": {
-      "version": "1.5.10",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
-      "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
-      "requires": {
-        "debug": "=3.1.0"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-          "requires": {
-            "ms": "2.0.0"
-          }
-        }
-      }
-    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -2570,11 +2536,6 @@
       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
       "dev": true
     },
-    "jquery": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
-      "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
-    },
     "js-base64": {
       "version": "2.5.1",
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
@@ -3082,7 +3043,8 @@
     "ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
     },
     "nan": {
       "version": "2.14.0",
diff --git a/package.json b/package.json
index 0198f0ccc..0d1afbb0c 100644
--- a/package.json
+++ b/package.json
@@ -21,11 +21,9 @@
     "webpack-cli": "^3.3.2"
   },
   "dependencies": {
-    "axios": "^0.19.0",
     "clipboard": "^2.0.4",
     "codemirror": "^5.47.0",
     "dropzone": "^5.5.1",
-    "jquery": "^3.4.1",
     "markdown-it": "^8.4.2",
     "markdown-it-task-lists": "^2.1.1",
     "sortablejs": "^1.9.0",
diff --git a/readme.md b/readme.md
index 5c400a0c9..07bebff05 100644
--- a/readme.md
+++ b/readme.md
@@ -137,11 +137,9 @@ The great people that have worked to build and improve BookStack can [be seen he
 These are the great open-source projects used to help build BookStack:
 
 * [Laravel](http://laravel.com/)
-* [jQuery](https://jquery.com/)
 * [TinyMCE](https://www.tinymce.com/)
 * [CodeMirror](https://codemirror.net)
 * [Vue.js](http://vuejs.org/)
-* [Axios](https://github.com/mzabriskie/axios)
 * [Sortable](https://github.com/SortableJS/Sortable) & [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)
 * [Google Material Icons](https://material.io/icons/)
 * [Dropzone.js](http://www.dropzonejs.com/)
diff --git a/resources/assets/js/components/breadcrumb-listing.js b/resources/assets/js/components/breadcrumb-listing.js
index 44f7d89e0..11e1522db 100644
--- a/resources/assets/js/components/breadcrumb-listing.js
+++ b/resources/assets/js/components/breadcrumb-listing.js
@@ -60,7 +60,7 @@ class BreadcrumbListing {
             'entity_type': this.entityType,
         };
 
-        window.$http.get('/search/entity/siblings', {params}).then(resp => {
+        window.$http.get('/search/entity/siblings', params).then(resp => {
             this.entityListElem.innerHTML = resp.data;
         }).catch(err => {
             console.error(err);
diff --git a/resources/assets/js/components/page-comments.js b/resources/assets/js/components/page-comments.js
index 975ff5a82..cabce9139 100644
--- a/resources/assets/js/components/page-comments.js
+++ b/resources/assets/js/components/page-comments.js
@@ -1,4 +1,6 @@
 import MarkdownIt from "markdown-it";
+import {scrollAndHighlightElement} from "../services/util";
+
 const md = new MarkdownIt({ html: false });
 
 class PageComments {
@@ -25,8 +27,8 @@ class PageComments {
     handleAction(event) {
         let actionElem = event.target.closest('[action]');
         if (event.target.matches('a[href^="#"]')) {
-            let id = event.target.href.split('#')[1];
-            window.scrollAndHighlight(document.querySelector('#' + id));
+            const id = event.target.href.split('#')[1];
+            scrollAndHighlightElement(document.querySelector('#' + id));
         }
         if (actionElem === null) return;
         event.preventDefault();
@@ -132,7 +134,7 @@ class PageComments {
         this.formContainer.parentNode.style.display = 'block';
         this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
         this.formInput.focus();
-        window.scrollToElement(this.formInput);
+        this.formInput.scrollIntoView({behavior: "smooth"});
     }
 
     hideForm() {
diff --git a/resources/assets/js/components/page-display.js b/resources/assets/js/components/page-display.js
index a3879d006..b2c05ebc6 100644
--- a/resources/assets/js/components/page-display.js
+++ b/resources/assets/js/components/page-display.js
@@ -1,6 +1,7 @@
 import Clipboard from "clipboard/dist/clipboard.min";
 import Code from "../services/code";
 import * as DOM from "../services/dom";
+import {scrollAndHighlightElement} from "../services/util";
 
 class PageDisplay {
 
@@ -20,10 +21,12 @@ class PageDisplay {
 
         // Sidebar page nav click event
         const sidebarPageNav = document.querySelector('.sidebar-page-nav');
-        DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
-            window.components['tri-layout'][0].showContent();
-            this.goToText(child.getAttribute('href').substr(1));
-        });
+        if (sidebarPageNav) {
+            DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
+                window.components['tri-layout'][0].showContent();
+                this.goToText(child.getAttribute('href').substr(1));
+            });
+        }
     }
 
     goToText(text) {
@@ -35,11 +38,11 @@ class PageDisplay {
         });
 
         if (idElem !== null) {
-            window.scrollAndHighlight(idElem);
+            scrollAndHighlightElement(idElem);
         } else {
             const textElem = DOM.findText('.page-content > div > *', text);
             if (textElem) {
-                window.scrollAndHighlight(textElem);
+                scrollAndHighlightElement(textElem);
             }
         }
     }
diff --git a/resources/assets/js/index.js b/resources/assets/js/index.js
index f202c322e..c23615a88 100644
--- a/resources/assets/js/index.js
+++ b/resources/assets/js/index.js
@@ -1,6 +1,3 @@
-// Global Polyfills
-import "./services/dom-polyfills"
-
 // Url retrieval function
 window.baseUrl = function(path) {
     let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
@@ -11,27 +8,24 @@ window.baseUrl = function(path) {
 
 // Set events and http services on window
 import Events from "./services/events"
-import Http from "./services/http"
-let httpInstance = Http();
+import httpInstance from "./services/http"
+const eventManager = new Events();
 window.$http = httpInstance;
-window.$events = new Events();
+window.$events = eventManager;
 
 // Translation setup
 // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
 import Translations from "./services/translations"
-let translator = new Translations(window.translations);
+const translator = new Translations(window.translations);
 window.trans = translator.get.bind(translator);
 window.trans_choice = translator.getPlural.bind(translator);
 
-// Load in global UI helpers and libraries including jQuery
-import "./services/global-ui"
-
-// Set services on Vue
+// Make services available to Vue instances
 import Vue from "vue"
 Vue.prototype.$http = httpInstance;
-Vue.prototype.$events = window.$events;
+Vue.prototype.$events = eventManager;
 
-// Load vues and components
+// Load Vues and components
 import vues from "./vues/vues"
 import components from "./components"
 vues();
diff --git a/resources/assets/js/services/dom-polyfills.js b/resources/assets/js/services/dom-polyfills.js
deleted file mode 100644
index d32af9118..000000000
--- a/resources/assets/js/services/dom-polyfills.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * Polyfills for DOM API's
- */
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
-if (!Element.prototype.matches) {
-    Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Browser_compatibility
-if (!Element.prototype.closest) {
-    Element.prototype.closest = function (s) {
-        var el = this;
-        var ancestor = this;
-        if (!document.documentElement.contains(el)) return null;
-        do {
-            if (ancestor.matches(s)) return ancestor;
-            ancestor = ancestor.parentElement;
-        } while (ancestor !== null);
-        return null;
-    };
-}
\ No newline at end of file
diff --git a/resources/assets/js/services/global-ui.js b/resources/assets/js/services/global-ui.js
deleted file mode 100644
index 948e8e880..000000000
--- a/resources/assets/js/services/global-ui.js
+++ /dev/null
@@ -1,58 +0,0 @@
-// Global jQuery Config & Extensions
-
-import jQuery from "jquery"
-window.jQuery = window.$ = jQuery;
-
-/**
- * Scroll the view to a specific element.
- * @param {HTMLElement} element
- */
-window.scrollToElement = function(element) {
-    if (!element) return;
-    let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
-    let top = element.getBoundingClientRect().top + offset;
-    $('html, body').animate({
-        scrollTop: top - 60 // Adjust to change final scroll position top margin
-    }, 300);
-};
-
-/**
- * Scroll and highlight an element.
- * @param {HTMLElement} element
- */
-window.scrollAndHighlight = function(element) {
-    if (!element) return;
-    window.scrollToElement(element);
-    let color = document.getElementById('custom-styles').getAttribute('data-color-light');
-    let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
-    element.style.backgroundColor = color;
-    setTimeout(() => {
-        element.classList.add('selectFade');
-        element.style.backgroundColor = initColor;
-    }, 10);
-    setTimeout(() => {
-        element.classList.remove('selectFade');
-        element.style.backgroundColor = '';
-    }, 3000);
-};
-
-// Smooth scrolling
-jQuery.fn.smoothScrollTo = function () {
-    if (this.length === 0) return;
-    window.scrollToElement(this[0]);
-    return this;
-};
-
-// Making contains text expression not worry about casing
-jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
-    return function (elem) {
-        return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
-    };
-});
-
-// Detect IE for css
-if(navigator.userAgent.indexOf('MSIE')!==-1
-    || navigator.appVersion.indexOf('Trident/') > 0
-    || navigator.userAgent.indexOf('Safari') !== -1){
-    document.body.classList.add('flexbox-support');
-}
\ No newline at end of file
diff --git a/resources/assets/js/services/http.js b/resources/assets/js/services/http.js
index 1e50fe2ae..06cc6a04f 100644
--- a/resources/assets/js/services/http.js
+++ b/resources/assets/js/services/http.js
@@ -1,21 +1,146 @@
-import axios from "axios"
 
-function instance() {
-    let axiosInstance = axios.create({
-        headers: {
-            'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
-            'baseURL': window.baseUrl('')
-        }
+/**
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
+ * @param {String} url
+ * @param {Object} params
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function get(url, params = {}) {
+    return request(url, {
+        method: 'GET',
+        params,
     });
-    axiosInstance.interceptors.request.use(resp => {
-        return resp;
-    }, err => {
-        if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
-        if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error);
-        if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message);
-    });
-    return axiosInstance;
 }
 
+/**
+ * Perform a HTTP POST request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function post(url, data = null) {
+    return dataRequest('POST', url, data);
+}
 
-export default instance;
\ No newline at end of file
+/**
+ * Perform a HTTP PUT request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function put(url, data = null) {
+    return dataRequest('PUT', url, data);
+}
+
+/**
+ * Perform a HTTP PATCH request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function patch(url, data = null) {
+    return dataRequest('PATCH', url, data);
+}
+
+/**
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function performDelete(url, data = null) {
+    return dataRequest('DELETE', url, data);
+}
+
+/**
+ * Perform a HTTP request to the back-end that includes data in the body.
+ * Parses the body to JSON if an object, setting the correct headers.
+ * @param {String} method
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function dataRequest(method, url, data = null) {
+    const options = {
+        method: method,
+        body: data,
+    };
+
+    if (typeof data === 'object') {
+        options.headers = {'Content-Type': 'application/json'};
+        options.body = JSON.stringify(data);
+    }
+
+    return request(url, options)
+}
+
+/**
+ * Create a new HTTP request, setting the required CSRF information
+ * to communicate with the back-end. Parses & formats the response.
+ * @param {String} url
+ * @param {Object} options
+ * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ */
+async function request(url, options = {}) {
+    if (!url.startsWith('http')) {
+        url = window.baseUrl(url);
+    }
+
+    if (options.params) {
+        const urlObj = new URL(url);
+        for (let paramName of Object.keys(options.params)) {
+            const value = options.params[paramName];
+            if (typeof value !== 'undefined' && value !== null) {
+                urlObj.searchParams.set(paramName, value);
+            }
+        }
+        url = urlObj.toString();
+    }
+
+    const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
+    options = Object.assign({}, options, {
+        'credentials': 'same-origin',
+    });
+    options.headers = Object.assign({}, options.headers || {}, {
+        'baseURL': window.baseUrl(''),
+        'X-CSRF-TOKEN': csrfToken,
+    });
+
+    const response = await fetch(url, options);
+    const content = await getResponseContent(response);
+    return {
+        data: content,
+        headers: response.headers,
+        redirected: response.redirected,
+        status: response.status,
+        statusText: response.statusText,
+        url: response.url,
+        original: response,
+    }
+}
+
+/**
+ * Get the content from a fetch response.
+ * Checks the content-type header to determine the format.
+ * @param response
+ * @returns {Promise<Object|String>}
+ */
+async function getResponseContent(response) {
+    const responseContentType = response.headers.get('Content-Type');
+    const subType = responseContentType.split('/').pop();
+
+    if (subType === 'javascript' || subType === 'json') {
+        return await response.json();
+    }
+
+    return await response.text();
+}
+
+export default {
+    get: get,
+    post: post,
+    put: put,
+    patch: patch,
+    delete: performDelete,
+};
\ No newline at end of file
diff --git a/resources/assets/js/services/util.js b/resources/assets/js/services/util.js
index 727c723c6..b2f291872 100644
--- a/resources/assets/js/services/util.js
+++ b/resources/assets/js/services/util.js
@@ -24,4 +24,25 @@ export function debounce(func, wait, immediate) {
         timeout = setTimeout(later, wait);
         if (callNow) func.apply(context, args);
     };
-};
\ No newline at end of file
+};
+
+/**
+ * Scroll and highlight an element.
+ * @param {HTMLElement} element
+ */
+export function scrollAndHighlightElement(element) {
+    if (!element) return;
+    element.scrollIntoView({behavior: 'smooth'});
+
+    const color = document.getElementById('custom-styles').getAttribute('data-color-light');
+    const initColor = window.getComputedStyle(element).getPropertyValue('background-color');
+    element.style.backgroundColor = color;
+    setTimeout(() => {
+        element.classList.add('selectFade');
+        element.style.backgroundColor = initColor;
+    }, 10);
+    setTimeout(() => {
+        element.classList.remove('selectFade');
+        element.style.backgroundColor = '';
+    }, 3000);
+}
\ No newline at end of file
diff --git a/resources/assets/js/vues/components/autosuggest.js b/resources/assets/js/vues/components/autosuggest.js
index 4fe183f02..d76ee89f1 100644
--- a/resources/assets/js/vues/components/autosuggest.js
+++ b/resources/assets/js/vues/components/autosuggest.js
@@ -113,11 +113,13 @@ const methods = {
      */
     getSuggestions(input, params) {
         params.search = input;
-        let cacheKey = `${this.url}:${JSON.stringify(params)}`;
+        const cacheKey = `${this.url}:${JSON.stringify(params)}`;
 
-        if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
+        if (typeof ajaxCache[cacheKey] !== "undefined") {
+            return Promise.resolve(ajaxCache[cacheKey]);
+        }
 
-        return this.$http.get(this.url, {params}).then(resp => {
+        return this.$http.get(this.url, params).then(resp => {
             ajaxCache[cacheKey] = resp.data;
             return resp.data;
         });
diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js
index dd1d9d17a..6df12d16d 100644
--- a/resources/assets/js/vues/image-manager.js
+++ b/resources/assets/js/vues/image-manager.js
@@ -57,14 +57,14 @@ const methods = {
     },
 
     async fetchData() {
-        let query = {
+        const params = {
             page,
             search: this.searching ? this.searchTerm : null,
             uploaded_to: this.uploadedTo || null,
             filter_type: this.filter,
         };
 
-        const {data} = await this.$http.get(baseUrl, {params: query});
+        const {data} = await this.$http.get(baseUrl, params);
         this.images = this.images.concat(data.images);
         this.hasMore = data.has_more;
         page++;