diff --git a/.gitignore b/.gitignore
index 3367cdfe57facc5bbfc21b25201f778fba23d8cf..08d3037e0fd63c7357449e3bb137fb75f2217754 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,12 +2,18 @@
 __pycache__
 
 *.json
+*.log
 
 *~
 
+# Package managers
 venv*
+node_modules/
+*webpack-stats.json
+
 .python-version
 
+
 media/temp
 
 SCIPOST_JOURNALS
@@ -16,4 +22,4 @@ UPLOADS
 docs/_build
 local_files
 
-whoosh_index
\ No newline at end of file
+whoosh_index
diff --git a/README.md b/README.md
index 68c8f64bb480dd555f384ce3796a0a3d6af0be53..9bb43e0733105923388167cd35fb02d0892ddd43 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@ The complete scientific publication portal
 
 ## Dependencies
 SciPost is written in Python 3.5 using Django and requires PostgreSQL 9.3 or
-higher. Python dependencies are listed in `requirements.txt`.
+higher. Python dependencies are listed in `requirements.txt`. Frontend dependencies are managed by [NPM](https://www.npmjs.com/) in package.json.
 
 ## Getting started
 
@@ -28,6 +28,27 @@ Now install dependencies:
 (scipostenv) $ pip install -r requirements.txt
 ```
 
+### Frontend dependencies
+[NPM](https://www.npmjs.com/) will take care of frontend dependencies. To install all packages now run:
+
+```shell
+(scipostenv) $ npm install
+```
+
+### Module bundler
+[Webpack](http://webpack.github.io/docs/what-is-webpack.html) takes care of assets in the `scipost/static/scipost/assets` folder. To (re)compile all assets, simply run:
+
+```shell
+(scipostenv) $ npm run webpack
+```
+
+While editing assets, it is helpfull to put webpack in _watch_ mode. This will recompile your assets every time you edit them. To do so, instead of the above command, run:
+
+```shell
+(scipostenv) $ npm run webpack-live
+```
+
+
 ### Host-specific settings
 In this project, host-specific settings are defined in the `scipost-host-settings.json` file in the directory *above* the project root. The structure is as follows:
 
diff --git a/SciPost_v1/settings.py b/SciPost_v1/settings.py
index 66601f8063a94a36a6fb7b0024f3d14241da9ae5..e29c39cd4a59cddf46b5cfce84a26ae00f24cd1e 100644
--- a/SciPost_v1/settings.py
+++ b/SciPost_v1/settings.py
@@ -80,6 +80,7 @@ INSTALLED_APPS = (
     'scipost',
     'submissions',
     'theses',
+    'webpack_loader'
 )
 
 HAYSTACK_CONNECTIONS = {
@@ -175,6 +176,21 @@ USE_TZ = True
 
 STATIC_URL = host_settings["STATIC_URL"]
 STATIC_ROOT = host_settings["STATIC_ROOT"]
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, 'static'),
+)
+
+# Webpack handling the static bundles
+WEBPACK_LOADER = {
+    'DEFAULT': {
+        'CACHE': not DEBUG,
+        'BUNDLE_DIR_NAME': 'bundles/',
+        'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
+        'POLL_INTERVAL': 0.1,
+        'TIMEOUT': None,
+        'IGNORE': ['.+\.hot-update.js', '.+\.map']
+    }
+}
 
 # Email
 EMAIL_BACKEND = host_settings["EMAIL_BACKEND"]
diff --git a/requirements.txt b/requirements.txt
index 896a004561e886d51cf0dd50fda51b359dd34b95..a68b81195e9b923cdab7a7310edd8ff9a28ae769 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ django-mathjax==0.0.5
 django-mptt==0.8.6
 django-simple-captcha==0.5.3
 django-sphinxdoc==1.5.1
+django-webpack-loader==0.4.1
 djangorestframework==3.5.3
 docutils==0.12
 feedparser==5.2.1
diff --git a/scipost/static/scipost/assets/css/style.css b/scipost/static/scipost/assets/css/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scipost/templates/scipost/base.html b/scipost/templates/scipost/base.html
index 23a60758b0fdf262600e580d527bc61a30306e2e..ace0fe23bc2cb14db9e7b57de194ccb879fec9e1 100644
--- a/scipost/templates/scipost/base.html
+++ b/scipost/templates/scipost/base.html
@@ -1,9 +1,13 @@
+{% load render_bundle from webpack_loader %}
+
 <!DOCTYPE html>
 <html lang="en">
   <head>
     {% load staticfiles %}
     <link rel="stylesheet" type="text/css" href="{% static 'scipost/SciPost.css' %}" />
 
+    {% render_bundle 'main' 'css' %}
+
     <link rel="shortcut icon" href="{% static 'scipost/images/scipost_favicon.png' %}"/>
 
     <script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
@@ -34,6 +38,8 @@
     {% endblock content %}
 
     {% include 'scipost/footer.html' %}
+
+    {% render_bundle 'main' 'js' %}
   </body>
 
 </html>
diff --git a/static/bundles/css/main-74c70b907a6530bfb5dd.css b/static/bundles/css/main-74c70b907a6530bfb5dd.css
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/static/bundles/js/main-74c70b907a6530bfb5dd.js b/static/bundles/js/main-74c70b907a6530bfb5dd.js
new file mode 100644
index 0000000000000000000000000000000000000000..e99d5573e922460823d91e4481531580de97e891
--- /dev/null
+++ b/static/bundles/js/main-74c70b907a6530bfb5dd.js
@@ -0,0 +1,57 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId])
+/******/ 			return installedModules[moduleId].exports;
+
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			exports: {},
+/******/ 			id: moduleId,
+/******/ 			loaded: false
+/******/ 		};
+
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ 		// Flag the module as loaded
+/******/ 		module.loaded = true;
+
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+
+
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "/static/bundles/";
+
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+	module.exports = __webpack_require__(1);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+	// removed by extract-text-webpack-plugin
+
+/***/ }
+/******/ ]);
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..57789e0dee28a59ad704306e7762c469cfd316d6
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,40 @@
+var BundleTracker = require('webpack-bundle-tracker');
+var CleanWebpackPlugin = require('clean-webpack-plugin');
+var ExtractTextPlugin = require("extract-text-webpack-plugin");
+var glob = require("glob");
+var path_bundles = __dirname + '/static/bundles/';
+
+module.exports = {
+    context: __dirname,
+    entry: {
+        main: glob.sync("./scipost/static/scipost/assets/css/*.css")
+    },
+    output: {
+        path: path_bundles,
+        publicPath: '/static/bundles/',
+        filename: "js/[name]-[hash].js",
+    },
+    module: {
+        loaders: [
+            {
+                test: /\.css$/,
+                loader: ExtractTextPlugin.extract("style-loader", "css-loader")
+            },
+            // Optionally extract less files (yeay!)
+            {
+                test: /\.less$/,
+                loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader")
+            }
+        ]
+    },
+    plugins: [
+        new BundleTracker({filename: './webpack-stats.json'}),
+        new ExtractTextPlugin('css/[name]-[hash].css'),
+        new CleanWebpackPlugin(['css', 'js'], {
+            root: path_bundles,
+            verbose: true,
+            dry: false,
+            exclude: []
+        })
+    ]
+}