diff --git a/packages/file-handling/README.md b/packages/file-handling/README.md
index 0d5510f319c6ddc0e8b68d1298fe6c4a2bb17cbf..69e46997818fc6783cddc973c7ece423e2c3b619 100644
--- a/packages/file-handling/README.md
+++ b/packages/file-handling/README.md
@@ -27,10 +27,16 @@ Files will be uploaded sequentially (not parallel) to prevent overburdening the
     - example `<vpu-fileupload allowed-mime-types='image/png,text/plain'></vpu-fileupload>` ... PNGs or TXTs only
     - example `<vpu-fileupload allowed-mime-types='*/*'></vpu-fileupload>` ... all file types (default)
 - `disabled` (optional): disable input control
-    - example `<vpu-fileupload disabled>`
+    - example `<vpu-fileupload disabled></vpu-fileupload>`
 - `decompress-zip` (optional): decompress zip file and queue the contained files (including files in folders)
-    - example `<vpu-fileupload decompress-zip>`
+    - example `<vpu-fileupload decompress-zip></vpu-fileupload>`
     - mime types of `allowed-mime-types` will also be checked for the files in the zip file
+- `nextcloud-auth-url` (optional): Nextcloud Auth Url to use with the Nextcloud file picker
+    - example `<vpu-fileupload nextcloud-auth-url="http://localhost:8081/index.php/apps/webapppassword"></vpu-fileupload>`
+    - `nextcloud-web-dav-url` also needs to be set for the Nextcloud file picker to be active
+- `nextcloud-web-dav-url` (optional): Nextcloud WebDav Url to use with the Nextcloud file picker
+    - example `<vpu-fileupload nextcloud-web-dav-url="http://localhost:8081/remote.php/dav/files"></vpu-fileupload>`
+    - `nextcloud-auth-url` also needs to be set for the Nextcloud file picker to be active
 
 ## Local development
 
diff --git a/packages/file-handling/package.json b/packages/file-handling/package.json
index e18aa0c4f1fd8713230360667aff501760355adf..bfbc93924c3ecfb2efd750697eecbaa3efb9eade 100644
--- a/packages/file-handling/package.json
+++ b/packages/file-handling/package.json
@@ -29,7 +29,9 @@
     "i18next": "^19.4.2",
     "lit-element": "^2.1.0",
     "lit-html": "^1.1.1",
-    "material-design-icons-svg": "^3.0.0"
+    "material-design-icons-svg": "^3.0.0",
+    "tabulator-tables": "^4.7.0",
+    "webdav": "^3.3.0"
   },
   "scripts": {
     "clean": "rm dist/*",
diff --git a/packages/file-handling/rollup.config.js b/packages/file-handling/rollup.config.js
index 8fbc8e44a727ed10ab6ec1f6859cca7c11acb20f..644db66e82b8408da8c446cf6b679d01b2330de3 100644
--- a/packages/file-handling/rollup.config.js
+++ b/packages/file-handling/rollup.config.js
@@ -44,6 +44,7 @@ export default {
                 {src: 'assets/index.html', dest: 'dist'},
                 {src: 'assets/favicon.ico', dest: 'dist'},
                 {src: 'node_modules/material-design-icons-svg/paths/*.json', dest: 'dist/local/vpu-common/icons'},
+                {src: 'node_modules/tabulator-tables/dist/css', dest: 'dist/local/fileupload/tabulator-tables'},
             ],
         }),
         (process.env.ROLLUP_WATCH === 'true') ? serve({contentBase: 'dist', host: '127.0.0.1', port: 8002}) : false
diff --git a/packages/file-handling/src/fileupload.js b/packages/file-handling/src/fileupload.js
index 849e7ed4c45d459bd6f762f605c955310ab860de..b5a993ecf0026ca41f5634302c78cd2d9d31aa73 100644
--- a/packages/file-handling/src/fileupload.js
+++ b/packages/file-handling/src/fileupload.js
@@ -6,6 +6,8 @@ import VPULitElement from 'vpu-common/vpu-lit-element';
 import * as commonUtils from "vpu-common/utils";
 import {Icon, MiniSpinner} from 'vpu-common';
 import * as commonStyles from 'vpu-common/styles';
+import {NextcloudFilePicker} from "./vpu-nextcloud-file-picker";
+import {classMap} from 'lit-html/directives/class-map.js';
 
 
 function mimeTypesToAccept(mimeTypes) {
@@ -34,6 +36,8 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) {
         super();
         this.lang = 'de';
         this.url = '';
+        this.nextcloudAuthUrl = '';
+        this.nextcloudWebDavUrl = '';
         this.dropArea = null;
         this.allowedMimeTypes = '*/*';
         this.text = '';
@@ -53,6 +57,7 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) {
         return {
             'vpu-icon': Icon,
             'vpu-mini-spinner': MiniSpinner,
+            'vpu-nextcloud-file-picker': NextcloudFilePicker,
         };
     }
 
@@ -64,6 +69,8 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) {
             lang: { type: String },
             url: { type: String },
             allowedMimeTypes: { type: String, attribute: 'allowed-mime-types' },
+            nextcloudAuthUrl: { type: String, attribute: 'nextcloud-auth-url' },
+            nextcloudWebDavUrl: { type: String, attribute: 'nextcloud-web-dav-url' },
             text: { type: String },
             buttonLabel: { type: String, attribute: 'button-label' },
             uploadInProgress: { type: Boolean, attribute: false },
@@ -451,6 +458,14 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) {
                            multiple
                            accept="${mimeTypesToAccept(allowedMimeTypes)}"
                            name='file'>
+                    <vpu-nextcloud-file-picker class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}"
+                                               ?disabled="${this.uploadInProgress || this.disabled}"
+                                               lang="${this.lang}"
+                                               auth-url="${this.nextcloudAuthUrl}"
+                                               web-dav-url="${this.nextcloudWebDavUrl}"
+                                               @vpu-nextcloud-file-picker-file-downloaded="${(event) => {
+                                                   this.queueFile(event.detail.file);
+                                               }}"></vpu-nextcloud-file-picker>
                     <label class="button is-primary" for="fileElem" ?disabled="${this.disabled}">
                         <vpu-icon style="display: ${this.uploadInProgress ? "inline-block" : "none"}" name="lock"></vpu-icon>
                         ${this.buttonLabel || i18n.t('upload-label')}
diff --git a/packages/file-handling/src/vpu-nextcloud-file-picker.js b/packages/file-handling/src/vpu-nextcloud-file-picker.js
new file mode 100644
index 0000000000000000000000000000000000000000..fed15c0f427244d4e21719cbf47a1226b730f8dd
--- /dev/null
+++ b/packages/file-handling/src/vpu-nextcloud-file-picker.js
@@ -0,0 +1,254 @@
+import {i18n} from './i18n';
+import {css, html} from 'lit-element';
+import {ScopedElementsMixin} from '@open-wc/scoped-elements';
+import VPULitElement from 'vpu-common/vpu-lit-element';
+import {MiniSpinner} from 'vpu-common';
+import * as commonUtils from 'vpu-common/utils';
+import * as commonStyles from 'vpu-common/styles';
+// `import {createClient} from 'webdav/web';` didn't seem to work if fileupload demo page is built
+// `import createClient from 'webdav/web';` didn't seem to work if Signature project is built
+import * as webDavWeb from 'webdav/web';
+import {classMap} from 'lit-html/directives/class-map.js';
+import {humanFileSize} from 'vpu-common/i18next';
+import Tabulator from 'tabulator-tables';
+
+/**
+ * NextcloudFilePicker web component
+ */
+export class NextcloudFilePicker extends ScopedElementsMixin(VPULitElement) {
+    constructor() {
+        super();
+        this.lang = 'de';
+        this.authUrl = '';
+        this.webDavUrl = '';
+        this.loginWindow = null;
+        this.isPickerActive = false;
+        this.statusText = "";
+        this.lastDirectoryPath = "/";
+        this.directoryPath = "/";
+        this.webDavClient = null;
+        this.tabulatorTable = null;
+
+        this._onReceiveWindowMessage = this.onReceiveWindowMessage.bind(this);
+    }
+
+    static get scopedElements() {
+        return {
+            'vpu-mini-spinner': MiniSpinner,
+        };
+    }
+
+    /**
+     * See: https://lit-element.polymer-project.org/guide/properties#initialize
+     */
+    static get properties() {
+        return {
+            lang: { type: String },
+            authUrl: { type: String, attribute: "auth-url" },
+            webDavUrl: { type: String, attribute: "web-dav-url" },
+            isPickerActive: { type: Boolean, attribute: false },
+            statusText: { type: String, attribute: false },
+            directoryPath: { type: String, attribute: false },
+        };
+    }
+
+    update(changedProperties) {
+        changedProperties.forEach((oldValue, propName) => {
+            switch (propName) {
+                case "lang":
+                    i18n.changeLanguage(this.lang);
+                    break;
+            }
+        });
+
+        super.update(changedProperties);
+    }
+
+    disconnectedCallback() {
+        window.removeEventListener('message', this._onReceiveWindowMessage);
+        super.disconnectedCallback();
+      }
+
+    connectedCallback() {
+        super.connectedCallback();
+
+        this.updateComplete.then(() => {
+            // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
+            window.addEventListener('message', this._onReceiveWindowMessage);
+
+            // http://tabulator.info/docs/4.7
+            // TODO: format size and lastmod
+            // TODO: translation of column headers
+            // TODO: mime type icon
+            this.tabulatorTable = new Tabulator(this._("#directory-content-table"), {
+                layout: "fitDataStretch",
+                selectable: true,
+                columns: [
+                    {title: "Filename", field: "basename"},
+                    {title: "Size", field: "size", formatter: (cell, formatterParams, onRendered) => {
+                            return cell.getRow().getData().type === "directory" ? "" : humanFileSize(cell.getValue());}},
+                    {title: "Type", field: "type"},
+                    {title: "Mime", field: "mime"},
+                    {title: "Last modified", field: "lastmod", sorter: "date"},
+                ],
+                rowClick: (e, row) => {
+                    const data = row.getData();
+
+                    switch(data.type) {
+                        case "directory":
+                            this.directoryClicked(e, data);
+                            break;
+                        case "file":
+                            console.log("file selected", data);
+                            break;
+                    }
+                },
+            });
+        });
+    }
+
+    openFilePicker() {
+        // TODO: translation
+        this.statusText = "Auth in progress";
+        this.loginWindow = window.open(this.authUrl, "Nextcloud Login",
+            "width=400,height=400,menubar=no,scrollbars=no,status=no,titlebar=no,toolbar=no");
+    }
+
+    onReceiveWindowMessage(event) {
+        const data = event.data;
+        console.log("data", data);
+
+        if (data.type === "webapppassword") {
+            this.loginWindow.close();
+            // alert("Login name: " + data.loginName + "\nApp password: " + data.token);
+
+            const apiUrl = this.webDavUrl + "/" + data.loginName;
+
+            // https://github.com/perry-mitchell/webdav-client/blob/master/API.md#module_WebDAV.createClient
+            this.webDavClient = webDavWeb.createClient(
+                apiUrl,
+                {
+                    username: data.loginName,
+                    password: data.token
+                }
+            );
+
+            this.loadDirectory("/");
+        }
+    }
+
+    /**
+     * Loads the directory from WebDAV
+     *
+     * @param path
+     */
+    loadDirectory(path) {
+        // TODO: translation
+        this.statusText = "Loading directory from Nextcloud: " + path;
+        this.lastDirectoryPath = this.directoryPath;
+        this.directoryPath = path;
+
+        // https://github.com/perry-mitchell/webdav-client#getdirectorycontents
+        this.webDavClient
+            .getDirectoryContents(path, {details: true})
+            .then(contents => {
+                console.log("contents", contents);
+                this.statusText = "";
+                this.tabulatorTable.setData(contents.data);
+                this.isPickerActive = true;
+            }).catch(error => {
+            console.error(error.message);
+            this.statusText = error.message;
+            this.isPickerActive = false;
+        });
+    }
+
+    directoryClicked(event, file) {
+        this.loadDirectory(file.filename);
+        event.preventDefault();
+    }
+
+    downloadFiles(files) {
+        files.forEach((fileData) => this.downloadFile(fileData));
+    }
+
+    downloadFile(fileData) {
+        this.statusText = "Loading " + fileData.filename + "...";
+
+        // https://github.com/perry-mitchell/webdav-client#getfilecontents
+        this.webDavClient
+            .getFileContents(fileData.filename)
+            .then(contents => {
+                // create file to send via event
+                const file = new File([contents], fileData.basename, { type: fileData.mime });
+                console.log("binaryFile", file);
+
+                // send event
+                const data = {"file": file, "data": fileData};
+                const event = new CustomEvent("vpu-nextcloud-file-picker-file-downloaded",
+                    { "detail": data, bubbles: true, composed: true });
+                this.dispatchEvent(event);
+
+                this.statusText = "";
+            }).catch(error => {
+                console.error(error.message);
+                this.statusText = error.message;
+            });
+    }
+
+    /**
+     * Returns the parent directory path
+     *
+     * @returns {string} parent directory path
+     */
+    getParentDirectoryPath() {
+        let path = this.directoryPath.replace(/\/$/, "");
+        path = path.replace(path.split("/").pop(), "").replace(/\/$/, "");
+
+        return (path === "") ? "/" : path;
+    }
+
+    static get styles() {
+        // language=css
+        return css`
+            ${commonStyles.getGeneralCSS()}
+            ${commonStyles.getButtonCSS()}
+
+            .block {
+                margin-bottom: 10px;
+            }
+        `;
+    }
+
+    render() {
+        commonUtils.initAssetBaseURL('vpu-tabulator-table');
+        const tabulatorCss = commonUtils.getAssetURL('local/vpu-fileupload/tabulator-tables/css/tabulator.min.css');
+        console.log("tabulatorCss", tabulatorCss);
+
+        return html`
+            <link rel="stylesheet" href="${tabulatorCss}">
+            <div class="block">
+                <button class="button"
+                        title="${i18n.t('nextcloud-file-picker.open-nextcloud-file-picker')}"
+                        @click="${async () => { this.openFilePicker(); } }">${i18n.t('nextcloud-file-picker.open')}</button>
+            </div>
+            <div class="block ${classMap({hidden: this.statusText === ""})}">
+                <vpu-mini-spinner style="font-size: 0.7em"></vpu-mini-spinner>
+                ${this.statusText}
+            </div>
+            <div class="block ${classMap({hidden: !this.isPickerActive})}">
+                <h2>${this.directoryPath}</h2>
+                <button class="button is-small"
+                        title="${i18n.t('nextcloud-file-picker.folder-last')}"
+                        @click="${() => { this.loadDirectory(this.lastDirectoryPath); }}">&#8678;</button>
+                <button class="button is-small"
+                        title="${i18n.t('nextcloud-file-picker.folder-up')}"
+                        @click="${() => { this.loadDirectory(this.getParentDirectoryPath()); }}">&#8679;</button>
+                <table id="directory-content-table"></table>
+                <button class="button"
+                        title="${i18n.t('nextcloud-file-picker.folder-up')}"
+                        @click="${() => { this.downloadFiles(this.tabulatorTable.getSelectedData()); }}">${i18n.t('nextcloud-file-picker.select-files')}</button>
+            </div>
+        `;
+    }
+}