diff --git a/jsfileupload/__init__.py b/jsfileupload/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd7a85290b077bcb22329c144b75a25fe9e630a0
--- /dev/null
+++ b/jsfileupload/__init__.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+from .upload_widget import FileUpload
+from ._version import __version__, version_info
+
+from .nbextension import _jupyter_nbextension_paths
diff --git a/jsfileupload/_frontend.py b/jsfileupload/_frontend.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5bc60466f653df5348396654c024d7e929ccc93
--- /dev/null
+++ b/jsfileupload/_frontend.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+"""
+Information about the frontend package of the widgets.
+"""
+
+module_name = "jsfileupload"
+module_version = "^0.1.0"
diff --git a/jsfileupload/_version.py b/jsfileupload/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdb1be357a757978d8a6036dbfe2dc914f5e5d40
--- /dev/null
+++ b/jsfileupload/_version.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+version_info = (0, 1, 0, 'dev')
+__version__ = ".".join(map(str, version_info))
diff --git a/jsfileupload/nbextension/__init__.py b/jsfileupload/nbextension/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b525f3a00601fefa46355ec4bb29cd3a21cd57a3
--- /dev/null
+++ b/jsfileupload/nbextension/__init__.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC)
+# Distributed under the terms of the Modified BSD License.
+
+def _jupyter_nbextension_paths():
+    return [{
+        'section': 'notebook',
+        'src': 'nbextension/static',
+        'dest': 'jsfileupload',
+        'require': 'jsfileupload/extension'
+    }]
diff --git a/jsfileupload/nbextension/static/extension.js b/jsfileupload/nbextension/static/extension.js
new file mode 100644
index 0000000000000000000000000000000000000000..fed306efe40f66961d39363d4e339e87f09ccae6
--- /dev/null
+++ b/jsfileupload/nbextension/static/extension.js
@@ -0,0 +1,17 @@
+// Entry point for the notebook bundle containing custom model definitions.
+//
+define(function() {
+    "use strict";
+
+    window['requirejs'].config({
+        map: {
+            '*': {
+                'jsfileupload': 'nbextensions/jsfileupload/index',
+            },
+        }
+    });
+    // Export the required load_ipython_extension function
+    return {
+        load_ipython_extension : function() {}
+    };
+});
diff --git a/jsfileupload/tests/__init__.py b/jsfileupload/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/jsfileupload/tests/conftest.py b/jsfileupload/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4ecae680762509d8c56db66878127e22a0fa505
--- /dev/null
+++ b/jsfileupload/tests/conftest.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+import pytest
+
+from ipykernel.comm import Comm
+from ipywidgets import Widget
+
+class MockComm(Comm):
+    """A mock Comm object.
+
+    Can be used to inspect calls to Comm's open/send/close methods.
+    """
+    comm_id = 'a-b-c-d'
+    kernel = 'Truthy'
+
+    def __init__(self, *args, **kwargs):
+        self.log_open = []
+        self.log_send = []
+        self.log_close = []
+        super(MockComm, self).__init__(*args, **kwargs)
+
+    def open(self, *args, **kwargs):
+        self.log_open.append((args, kwargs))
+
+    def send(self, *args, **kwargs):
+        self.log_send.append((args, kwargs))
+
+    def close(self, *args, **kwargs):
+        self.log_close.append((args, kwargs))
+
+_widget_attrs = {}
+undefined = object()
+
+
+@pytest.fixture
+def mock_comm():
+    _widget_attrs['_comm_default'] = getattr(Widget, '_comm_default', undefined)
+    Widget._comm_default = lambda self: MockComm()
+    _widget_attrs['_ipython_display_'] = Widget._ipython_display_
+    def raise_not_implemented(*args, **kwargs):
+        raise NotImplementedError()
+    Widget._ipython_display_ = raise_not_implemented
+
+    yield MockComm()
+
+    for attr, value in _widget_attrs.items():
+        if value is undefined:
+            delattr(Widget, attr)
+        else:
+            setattr(Widget, attr, value)
diff --git a/jsfileupload/tests/test_example.py b/jsfileupload/tests/test_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..112621d894159fbc262d03655c865e05d3cd6de6
--- /dev/null
+++ b/jsfileupload/tests/test_example.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+import pytest
+
+from ..example import ExampleWidget
+
+
+def test_example_creation_blank():
+    w = ExampleWidget()
+    assert w.value == 'Hello World'
diff --git a/jsfileupload/tests/test_nbextension_path.py b/jsfileupload/tests/test_nbextension_path.py
new file mode 100644
index 0000000000000000000000000000000000000000..91936058938f438ac5c0f1d69ded57525ec1205f
--- /dev/null
+++ b/jsfileupload/tests/test_nbextension_path.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+
+def test_nbextension_path():
+    # Check that magic function can be imported from package root:
+    from jsfileupload import _jupyter_nbextension_paths
+    # Ensure that it can be called without incident:
+    path = _jupyter_nbextension_paths()
+    # Some sanity checks:
+    assert len(path) == 1
+    assert isinstance(path[0], dict)
diff --git a/jsfileupload/upload_widget.py b/jsfileupload/upload_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..5bbc0b5d392b5d096156f4e41ad24420fae66206
--- /dev/null
+++ b/jsfileupload/upload_widget.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright (c) Juelich Supercomputing Centre (JSC).
+# Distributed under the terms of the Modified BSD License.
+
+"""
+FileUpload Widget using the Jupyter Notebook Server RestAPI.
+Can handle large files by uploading files in chunks.
+"""
+from ipywidgets import ValueWidget
+from ipywidgets import register, widget_serialization
+from ipywidgets.widgets.trait_types import InstanceDict
+from ipywidgets.widgets.widget_button import ButtonStyle
+from ipywidgets.widgets.widget_description import DescriptionWidget
+
+from traitlets import (
+     default, Unicode, Dict, List, Int, Bool, Bytes, CaselessStrEnum
+)
+from ._frontend import module_name, module_version
+
+
+class FileUpload(DescriptionWidget, ValueWidget):
+    """FileUpload Widget. Uploads via the Jupyter Notebook Server RestAPI.
+
+    The widget is able to upload large files by uploading them in chunks.
+    The file contents will however not be directly available over the widget,
+    but will have to be read into the notebook seperately.
+    """
+    _model_name = Unicode('FileUploadModel').tag(sync=True)
+    _model_module = Unicode(module_name).tag(sync=True)
+    _model_module_version = Unicode(module_version).tag(sync=True)
+    _view_name = Unicode('FileUploadView').tag(sync=True)
+    _view_module = Unicode(module_name).tag(sync=True)
+    _view_module_version = Unicode(module_version).tag(sync=True)
+
+    accept = Unicode(help='File types to accept, empty string for all').tag(sync=True)
+    multiple = Bool(help='If True, allow for multiple files upload').tag(sync=True)
+    disabled = Bool(help='Enable or disable button').tag(sync=True)
+    icon = Unicode('folder', help="Font-awesome icon name, without the 'fa-' prefix.").tag(sync=True)
+    button_style = CaselessStrEnum(
+        values=['primary', 'success', 'info', 'warning', 'danger', ''], default_value='',
+        help="""Use a predefined styling for the button.""").tag(sync=True)
+    style = InstanceDict(ButtonStyle).tag(sync=True, **widget_serialization)
+    metadata = List(Dict(), help='List of file metadata').tag(sync=True)
+
+    # Needed for uploading using the Notebook Server RestAPI.
+    token = Unicode(help='Jupyter API token').tag(sync=True)
+    upload_url = Unicode('http://localhost:8888/api/contents/',
+                        help='http(s)://<notebook_url>/api/contents/<path>').tag(sync=True)
+
+    # Variables set on the JavaScript side.
+    files = List().tag(sync=True)
+    responses = List().tag(sync=True)
+    finished = Bool(False).tag(sync=True)
+    _upload = Bool(False).tag(sync=True)
+
+    
+    def __init__(self, upload_url='http://localhost:8888/api/contents/', token='', *args, **kwargs):
+        """Args:
+            upload_url (str): Jupyter notebook URL appended by api/contents/<path>/. Directories on <path> must already exist.
+            token (str): Jupyter notebook authentication token.
+        """
+        super(FileUpload, self).__init__(*args, **kwargs)
+        self.upload_url = upload_url
+        self.token = token
+
+    @default('description')
+    def _default_description(self):
+        return 'Browse'
+
+    def upload(self):
+        """Uploads file(s) via the JS fetch API."""
+        self._upload = True
\ No newline at end of file