diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c225054c8b172c20d38ebf8a1c1561ecb55ded..bba61d8e1fac23c4aaeecac2ca0bd16d4e40827e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added (for new features, dependecies etc.)
 
+* new `navbar.add_tool` method which adds a button to submenu of the navbar.
 * new CSS classes for the styling of the default image and video preview of the
   `ext_bottom_line` module: `caosdb-v-bottom-line-image-preview` and
   `caosdb-v-bottom-line-video-preview`.
@@ -17,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
     * `edit_mode` prevents the user from editing old versions of an entity.
 
 ### Changed (for changes in existing functionality)
+- The `navbar.add_button` signatur changed to `add_button(button, options)`.
+  See the doc string of that method for more information.
 - added a layout argument to the create_plot function of ext_bottom_line
 
 ### Deprecated (for soon-to-be removed features) 
diff --git a/makefile b/makefile
index d9091f32004cfe9c25e28c9e75a3cc18471ac73b..aa0b045badaf4d923a0f425b8a7358373e7a3e51 100644
--- a/makefile
+++ b/makefile
@@ -155,7 +155,7 @@ cp-ext:
 cp-ext-test:
 	for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \
 		echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \
-		sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \
+		sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(TEST_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \
 	done
 	mkdir -p $(PUBLIC_DIR)/html
 	for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \
diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css
index 21a73e3acd96337b6dd6adec7bf1a024cac91e6c..d24e33fb36b96e29fb2f30039e74a06082bdb1ac 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -27,6 +27,26 @@ body {
     flex-direction: column;
 }
 
+.caosdb-v-navbar-toolbox li a:hover,
+.caosdb-v-navbar-toolbox li input:hover,
+.caosdb-v-navbar-toolbox li button:hover {
+    background-color: #f5f5f5;
+}
+
+.caosdb-v-navbar-toolbox li a,
+.caosdb-v-navbar-toolbox li input,
+.caosdb-v-navbar-toolbox li button {
+    display: block;
+    padding: 3px 20px;
+    border: none;
+    width: 100%;
+    text-align: left;
+    margin-top: 0px;
+    margin-bottom: 0px;
+    text-decoration: none;
+    background-color: transparent;
+}
+
 .caosdb-v-bottom-line-image-preview > img {
     max-width: 400px;
     max-height: 280px;
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index b496c3b7a176828cf99a3efd017d591dde2e937b..df93703d83fafde15908efb40c3ae578824980d9 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -26,7 +26,7 @@
 
 window.addEventListener('error', (e) => globalError(e.error));
 
-var globalError = function(error) {
+var globalError = function (error) {
     var stack = error.stack;
     var message = "Error! Please help to make CaosDB better! Copy this message and send it via email to info@indiscale.com.\n\n";
     message += error.toString();
@@ -40,7 +40,7 @@ var globalError = function(error) {
     throw error;
 }
 
-var globalClassNames = new function() {
+var globalClassNames = new function () {
     this.NotificationArea = "caosdb-preview-notification-area";
     this.WaitingNotification = "caosdb-preview-waiting-notification";
     this.ErrorNotification = "caosdb-preview-error-notification";
@@ -49,62 +49,192 @@ var globalClassNames = new function() {
 /**
  * navbar module contains convenience functions for the navbar.
  */
-this.navbar = new function() {
+this.navbar = new function () {
 
     var logger = log.getLogger("navbar");
     this.logger = logger;
     /**
-     * Add a button to the navbar (left navbar, append right), wrapped in a LI
-     * element.
+     * Add a button to the navbar.
      *
      * If the param `button` is a string then a suitable BUTTON element will be
-     * created. If the button is otherwise an HTMLElement it will just be
-     * wrapped and appended to the navbar.
+     * created. If the button is otherwise an HTMLElement or an array of
+     * HTMLElements it will just be wrapped, styled and appended to the navbar.
      *
-     * The optional `click_callback` function is bound to the click event.
+     * The optional `options` object knows the following optional keys:
+     *     @property {function} callback - a function which will be bound to the
+     *         click event of the button.
+     *     @property {string} title - the title attribute (a kind of tooltip)
+     *         of the button.
+     *     @property {HTMLElement} menu - where to append the button. Defaults
+     *         to the navbar itself.
      *
-     * @param {String|HTMLElement} button - add this button to navbar
-     * @param {function} [click_callback] - callback function for click
+     * If the `options["menu"] parameter is not set, the button is appended
+     * to the navbar directly and appears left of all previously appended
+     * children.
+     *
+     *
+     * @param {String|HTMLElement|HTMLElement[]} button - the button
+     * @param {object} [options] - further options
      * @return {HTMLElement} wrapper of the new button
      */
-    this.add_button = function(button, click_callback) {
+    this.add_button = function (button, options) {
+        logger.trace("enter add_button", button, options);
+
+        // assure that button parameter is {String|HTMLElement|HTMLElement[]}
+        if (typeof button === "undefined") {
+            throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button);
+        } else if (Array.isArray(button)) {
+            for (const element of button) {
+                if (!(element instanceof HTMLElement)) {
+                    throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], element was " + typeof element);
+                }
+            }
+        } else if (!(typeof button === "string" || button instanceof String || button instanceof HTMLElement)) {
+            throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button);
+        }
+
+        // default: empty options
+        const _options = options || {};
 
         var button_elem = button;
         if (typeof button === "string" || button instanceof String) {
             // create button element from string
             button_elem = $('<button>' + button + '</button>');
         }
-        $(button_elem).toggleClass("navbar-btn", true);
-        $(button_elem).toggleClass("btn", true);
-        $(button_elem).toggleClass("btn-link", true);
+
+        // set title
+        const title = _options["title"];
+        if (typeof _options !== "undefined") {
+            $(button_elem).attr("title", title);
+        }
+
 
         // bind click
-        if(typeof click_callback === "function") {
+        const click_callback = _options["callback"]
+        if (typeof click_callback === "function") {
             $(button_elem).click(click_callback);
         }
 
         // wrapp button
         let wrapper = $("<li></li>").append(button_elem);
-        $('#top-navbar').find("ul.caosdb-navbar").first().append(wrapper);
 
+
+        // menu defaults to the navbar
+        const menu = _options["menu"] || this.get_navbar();
+
+        if ($(menu).is("ul.caosdb-navbar")) {
+            // special styling for buttons which are added directly to the
+            // navbar
+            $(button_elem)
+                .toggleClass("navbar-btn", true)
+                .toggleClass("btn", true)
+                .toggleClass("btn-link", true);
+        }
+
+        logger.debug("add", wrapper, "to", menu);
+        $(menu).append(wrapper);
+
+        logger.trace("leave add_button", wrapper[0]);
         return wrapper[0];
     }
 
-    this.init = function() {
+    this.init = function () {
         $("nav.navbar-fixed-top")
-            .on("shown.bs.collapse", function(e) {
+            .on("shown.bs.collapse", function (e) {
                 logger.trace("navbar expands", e);
             })
-            .on("hidden.bs.collapse", function(e) {
+            .on("hidden.bs.collapse", function (e) {
                 logger.trace("navbar shrinks", e);
             });
     }
 
+
+    /**
+     * Create initially empty tool box dropdown and append to navbar.
+     *
+     * The returned element is the drop-down menu which will eventually contain
+     * the tool buttons. That means, the buttons can be added directly to the
+     * returned element to appear in the drop-down menu.
+     *
+     * @return {HTMLElement} the dropdown-menu.
+     */
+    this.init_toolbox = function (name) {
+        var button = $(`<a class="dropdown-toggle"
+            data-toggle="dropdown" href="#">${name}
+            <span class="caret"></span></a>`)[0];
+
+        var menu = $(`<ul
+            class="caosdb-v-navbar-toolbox
+                caosdb-f-navbar-toolbox
+                dropdown-menu"
+            data-toolbox-name="${name}"/>`)[0];
+
+        const wrapper = this.add_button([button, menu]);
+        $(wrapper).toggleClass("dropdown", true)
+            .children()
+            .toggleClass("btn", false)
+            .toggleClass("btn-link", false)
+            .toggleClass("navbar-btn", false);
+
+        return menu;
+    }
+
+    /**
+     * Add a tool to a toolbox.
+     *
+     * If the button is a string a new button element is created showing the
+     * string as its label. Otherwise the button should be an HTMLElement which
+     * is directly added to the toolbox.
+     * toolbox.
+     *
+     * The `callback` is a function which will be bound to the click event of
+     * the button.
+     *
+     * The passed or created button is wrapped in a LI element and added to the
+     * toolbox. The wrapper of the button is returned
+     *
+     * The optional `options` object knows the following optional keys:
+     *     @property {function} callback - a function which will be bound to the
+     *         click event of the button.
+     *     @property {string} title - the title attribute (a kind of tooltip)
+     *         of the button.
+     *
+     * @param {string|HTMLElement} button
+     * @param {string} [toolbox] - the name of the toolbox
+     * @param {object} [options] further options.
+     * @return {HTMLElement} the button wrapper.
+     */
+    this.add_tool = function (button, toolbox, options) {
+        const toolbox_element = this.get_toolbox(toolbox);
+
+        // put toolbox_element as menu into the options for `add_button`
+        const _options = $.extend({
+            menu: toolbox_element
+        }, options);
+
+        const wrapper = this.add_button(button, _options);
+        return wrapper;
+    }
+
+    this.get_navbar = function () {
+        return $('#top-navbar')
+            .find("ul.caosdb-navbar")[0];
+    }
+
+    this.get_toolbox = function (name) {
+        var toolbox = $(this.get_navbar()).find(".caosdb-f-navbar-toolbox[data-toolbox-name='" + name + "']");
+        if (toolbox.length > 0) {
+            return toolbox[0];
+        } else {
+            return this.init_toolbox(name);
+        }
+    }
+
 }
 
 
-this.caosdb_utils = new function() {
-    this.assert_string = function(obj, name, optional=false) {
+this.caosdb_utils = new function () {
+    this.assert_string = function (obj, name, optional = false) {
         if (typeof obj === "undefined" && optional) {
             return obj;
         }
@@ -114,7 +244,7 @@ this.caosdb_utils = new function() {
         throw new TypeError(name + " is expected to be a string, was " + typeof obj);
     }
 
-    this.assert_type = function(obj, type, name, optional=false) {
+    this.assert_type = function (obj, type, name, optional = false) {
         if (typeof obj === "undefined" && optional) {
             return obj;
         }
@@ -124,14 +254,14 @@ this.caosdb_utils = new function() {
         return obj;
     }
 
-    this.assert_html_element = function(obj, name) {
+    this.assert_html_element = function (obj, name) {
         if (typeof obj === "undefined" || !(obj instanceof HTMLElement)) {
             throw new TypeError(name + " is expected to be an HTMLElement, was " + typeof obj);
         }
         return obj;
     }
 
-    this.assert_array = function(obj, name, wrap_if_not_array) {
+    this.assert_array = function (obj, name, wrap_if_not_array) {
         if (Array.isArray(obj)) {
             return obj;
         } else if (wrap_if_not_array) {
@@ -144,8 +274,8 @@ this.caosdb_utils = new function() {
 /**
  * connection module contains all ajax calls.
  */
-this.connection = new function() {
-    this._init = function() {
+this.connection = new function () {
+    this._init = function () {
         /**
          * Send a get request.
          */
@@ -224,8 +354,8 @@ this.connection = new function() {
                     console.log(error);
                 } else if (error.status != null) {
                     throw new Error(
-                        "POST scripting returned with HTTP status " + error.status
-                            + " - " + error.statusText);
+                        "POST scripting returned with HTTP status " + error.status +
+                        " - " + error.statusText);
                 } else {
                     throw error;
                 }
@@ -284,7 +414,7 @@ this.connection = new function() {
         /**
          * Return the base path of the server.
          */
-        this.getBasePath = function() {
+        this.getBasePath = function () {
             var base = window.location.origin + "/";
             if (typeof window.sessionStorage.caosdbBasePath !== "undefined") {
                 base = window.sessionStorage.caosdbBasePath;
@@ -305,7 +435,7 @@ this.connection = new function() {
          * @param {string[]|number[]} ids
          * @return {string} the URI.
          */
-        this.getEntityUri = function(ids) {
+        this.getEntityUri = function (ids) {
             return this.getBasePath() + transaction.generateEntitiesUri(ids);
         }
     }
@@ -316,14 +446,14 @@ this.connection = new function() {
  * transformation module contains all code for tranforming xml into html via
  * xslt.
  */
-this.transformation = new function() {
+this.transformation = new function () {
     /**
      * remove all permission information from a server's response.
      *
      * @param {XMLDocument} xml
      * @return {XMLDocument} without <Permissions> tags.
      */
-    this.removePermissions = function(xml) {
+    this.removePermissions = function (xml) {
         $(xml).find('Permissions').remove();
         return xml
     }
@@ -424,10 +554,10 @@ this.transformation = new function() {
      *          be included.
      * @return {XMLDocument} a new style sheets with all template rules;
      */
-    this.mergeXsltScripts = function(xslMain, xslIncludes) {
+    this.mergeXsltScripts = function (xslMain, xslIncludes) {
         let ret = getXSLScriptClone(xslMain);
         for (var i = 0; i < xslIncludes.length; i++) {
-            $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function(index) {
+            $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function (index) {
                 $(ret.firstElementChild).append(this);
             });
         }
@@ -439,7 +569,7 @@ this.transformation = new function() {
  * transaction module contains all code for insertion, update and deletion of 
  * entities. Currently, only updates are implemented.
  */
-this.transaction = new function() {
+this.transaction = new function () {
     this.classNameUpdateForm = "caosdb-update-entity-form";
 
     /**
@@ -499,14 +629,14 @@ this.transaction = new function() {
      * @param {String[]} entityIds - An array of entity ids..
      * @return {String} The uri.
      */
-    this.generateEntitiesUri = function(entityIds) {
+    this.generateEntitiesUri = function (entityIds) {
         return "Entity/" + entityIds.join("&");
     }
 
     /**
      * Submodule for update transactions.
      */
-    this.update = new function() {
+    this.update = new function () {
         /**
          * Create a form for updating entities. It has only a textarea and a
          * submit button.
@@ -520,7 +650,7 @@ this.transaction = new function() {
          * @param {String} entityXmlStr, the old entity
          * @param {function} putCallback, the function which sends a put request.
          */
-        this.createUpdateForm = function(entityXmlStr, putCallback) {
+        this.createUpdateForm = function (entityXmlStr, putCallback) {
             // check the parameters
             if (putCallback == null) {
                 throw new Error("putCallback function must not be null.");
@@ -544,13 +674,13 @@ this.transaction = new function() {
             form.append(resetButton);
 
             // reset restores the original xmlStr
-            form.on('reset', function(e) {
+            form.on('reset', function (e) {
                 textarea.find('textarea').val(entityXmlStr);
                 return false;
             });
 
             // submit calls the putCallback
-            form.submit(function(e) {
+            form.submit(function (e) {
                 putCallback(e.target.updateXml.value);
                 return false;
             });
@@ -568,7 +698,7 @@ this.transaction = new function() {
          * @param {HTMLElement} entity, the div which represent the entity.
          * @return {Object} a state machine.
          */
-        this.updateSingleEntity = function(entity) {
+        this.updateSingleEntity = function (entity) {
             let updatePanel = transaction.update.createUpdateEntityPanel(transaction.update.createUpdateEntityHeading($(entity).find('.caosdb-entity-panel-heading')[0]));
             var app = new StateMachine({
                 transitions: [{
@@ -593,14 +723,14 @@ this.transaction = new function() {
                     to: 'final'
                 }, ],
             });
-            app.errorHandler = function(fn) {
+            app.errorHandler = function (fn) {
                 try {
                     fn();
                 } catch (e) {
                     setTimeout(() => app.resetApp(e), 1000);
                 }
             }
-            app.onInit = function(e, entity) {
+            app.onInit = function (e, entity) {
                 // remove entity
                 $(entity).hide();
                 app.errorHandler(() => {
@@ -617,7 +747,7 @@ this.transaction = new function() {
                 });
                 // retrieve old xml, trigger state change when response is ready
             };
-            app.onOpenForm = function(e, entityXmlStr) {
+            app.onOpenForm = function (e, entityXmlStr) {
                 app.errorHandler(() => {
                     // create and show Form
                     let form = transaction.update.createUpdateForm(entityXmlStr, (xmlstr) => {
@@ -626,14 +756,14 @@ this.transaction = new function() {
                     updatePanel.append(form);
                 });
             };
-            app.onResetApp = function(e, error) {
+            app.onResetApp = function (e, error) {
                 $(entity).show();
                 $(updatePanel).remove();
                 if (error != null) {
                     globalError(error);
                 }
             };
-            app.onShowUpdatedEntity = function(e, newentity) {
+            app.onShowUpdatedEntity = function (e, newentity) {
                 // remove updatePanel
                 updatePanel.remove();
                 // show new version of entity
@@ -641,7 +771,7 @@ this.transaction = new function() {
                 // remove old version
                 $(entity).remove();
             };
-            app.onSubmitForm = function(e, xmlstr) {
+            app.onSubmitForm = function (e, xmlstr) {
                 // remove form
                 $(updatePanel).find('form').remove();
 
@@ -673,7 +803,7 @@ this.transaction = new function() {
                     );
                 });
             };
-            app.onLeaveWaitPutEntity = function() {
+            app.onLeaveWaitPutEntity = function () {
                 // remove waiting notifications
                 removeAllWaitingNotifications(updatePanel);
             };
@@ -700,19 +830,19 @@ this.transaction = new function() {
             return xml2str(transformation.removePermissions(xml));
         }
 
-        this.createWaitRetrieveNotification = function() {
+        this.createWaitRetrieveNotification = function () {
             return createWaitingNotification("Retrieving xml and loading form. Please wait.");
         }
 
-        this.createWaitUpdateNotification = function() {
+        this.createWaitUpdateNotification = function () {
             return createWaitingNotification("Sending update to the server. Please wait.");
         }
 
-        this.createErrorInUpdatedEntityNotification = function() {
+        this.createErrorInUpdatedEntityNotification = function () {
             return createErrorNotification("The update was not successful.");
         }
 
-        this.addErrorNotification = function(elem, err) {
+        this.addErrorNotification = function (elem, err) {
             $(elem).append(err);
             return elem;
         }
@@ -724,7 +854,7 @@ this.transaction = new function() {
          * @param {HTMLElement} heading, the heading of the panel.
          * @return {HTMLElement} A div.
          */
-        this.createUpdateEntityPanel = function(heading) {
+        this.createUpdateEntityPanel = function (heading) {
             let panel = $('<div class="panel panel-default" style="border-color: blue;"/>');
             panel.append(heading);
             return panel[0];
@@ -739,7 +869,7 @@ this.transaction = new function() {
          * @param {HTMLElement} entityHeading, the heading of the entity.
          * @return {HTMLElement} the heading for the update panel.
          */
-        this.createUpdateEntityHeading = function(entityHeading) {
+        this.createUpdateEntityHeading = function (entityHeading) {
             let heading = entityHeading.cloneNode(true);
             let update = $('<span><h3>Update</h3></span>')[0];
             $(heading).children().slice(1).remove();
@@ -754,19 +884,19 @@ this.transaction = new function() {
          * @param {HTMLElement} entityPanel, the entity panel.
          * @return {HTMLElement} the heading.
          */
-        this.getEntityHeading = function(entityPanel) {
+        this.getEntityHeading = function (entityPanel) {
             return $(entityPanel).find('.caosdb-entity-panel-heading')[0];
         }
 
-        this.initUpdate = function(button) {
+        this.initUpdate = function (button) {
             transaction.update.updateSingleEntity(
                 $(button).closest('.caosdb-entity-panel')[0]
             );
         }
 
-        this.createCloseButton = function(close, callback) {
+        this.createCloseButton = function (close, callback) {
             let button = $('<button title="Cancel update" class="btn btn-link close" aria-label="Cancel update">&times;</button>');
-            button.bind('click', function() {
+            button.bind('click', function () {
                 $(this).closest(close).hide();
                 callback();
             });
@@ -776,7 +906,7 @@ this.transaction = new function() {
 }
 
 
-var paging = new function() {
+var paging = new function () {
 
     this.defaultPageLen = 10;
     /**
@@ -791,7 +921,7 @@ var paging = new function() {
      *            the number of entities which are currently shown on this
      *            page.
      */
-    this.initPaging = function(href, totalEntities) {
+    this.initPaging = function (href, totalEntities) {
 
         if (totalEntities == null) {
             return false;
@@ -841,7 +971,7 @@ var paging = new function() {
      *            the page the new uri shall point to
      * @return a string uri which points to the page denotes by the parameter
      */
-    this.getPageHref = function(uri_old, page) {
+    this.getPageHref = function (uri_old, page) {
         if (uri_old == null) {
             throw new Error("uri was null.");
         }
@@ -869,7 +999,7 @@ var paging = new function() {
      * @param uri An arbitrary URI, usually the current URI of the window.
      * @return The first part of the query segment which begins with 'P='
      */
-    this.getPSegmentFromUri = function(uri) {
+    this.getPSegmentFromUri = function (uri) {
         if (uri == null) {
             throw new Error("uri was null.");
         }
@@ -886,7 +1016,7 @@ var paging = new function() {
      *            a paging string
      * @return a String
      */
-    this.getPrevPage = function(P) {
+    this.getPrevPage = function (P) {
         if (P == null) {
             throw new Error("P was null");
         }
@@ -920,7 +1050,7 @@ var paging = new function() {
      *            total numbers of entities.
      * @return a String
      */
-    this.getNextPage = function(P, n) {
+    this.getNextPage = function (P, n) {
         // check n and P for null values and correct formatting
         if (n == null) {
             throw new Error("n was null");
@@ -950,8 +1080,8 @@ var paging = new function() {
     }
 };
 
-var queryForm = new function() {
-    this.init = function(form) {
+var queryForm = new function () {
+    this.init = function (form) {
         this.restoreLastQuery(form, () => window.sessionStorage.lastQuery);
         this.bindOnClick(form, (set) => {
             window.sessionStorage.lastQuery = set;
@@ -959,7 +1089,7 @@ var queryForm = new function() {
         });
     };
 
-    this.restoreLastQuery = function(form, getter) {
+    this.restoreLastQuery = function (form, getter) {
         if (form == null) {
             throw new Error("form was null");
         }
@@ -972,16 +1102,16 @@ var queryForm = new function() {
      * @value {string} query - the query string.
      * @param {string} paging - the paging string, e.g. 0L10.
      */
-    this.redirect = function(query, paging) {
+    this.redirect = function (query, paging) {
         var pagingparam = ""
-        if(paging && paging.length > 0) {
+        if (paging && paging.length > 0) {
             pagingparam = "P=" + paging + "&";
         }
         location.href = connection.getBasePath() + "Entity/?" + pagingparam + "query=" + query;
     }
 
-    this.bindOnClick = function(form, setter) {
-        if (setter == null || typeof(setter) !== 'function' || setter.length !== 1) {
+    this.bindOnClick = function (form, setter) {
+        if (setter == null || typeof (setter) !== 'function' || setter.length !== 1) {
             throw new Error("setter must be a function with one param");
         }
 
@@ -990,7 +1120,7 @@ var queryForm = new function() {
           and the click handler of the button.
           See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary.
           */
-        var submithandler = function() {
+        var submithandler = function () {
 
             // store current query
             var queryField = form.query;
@@ -1004,7 +1134,7 @@ var queryForm = new function() {
             setter(queryField.value);
 
             var paging = "";
-            if(form.P && !queryForm.isSelectQuery(queryField.value)) {
+            if (form.P && !queryForm.isSelectQuery(queryField.value)) {
                 paging = form.P.value
             }
 
@@ -1013,15 +1143,15 @@ var queryForm = new function() {
 
 
         // handler for the form
-	    form.onsubmit = function(e) {
-	        e.preventDefault();
-	        submithandler();
+        form.onsubmit = function (e) {
+            e.preventDefault();
+            submithandler();
             return false;
-	    };
+        };
 
         // same handler for the button
-        form.getElementsByClassName("caosdb-search-btn")[0].onclick = function() {
-	        submithandler();
+        form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () {
+            submithandler();
         };
     };
 
@@ -1031,7 +1161,7 @@ var queryForm = new function() {
      * @param {HTMLElement} query, the query to be tested.
      * @return {Boolean}
      */
-    this.isSelectQuery = function(query) {
+    this.isSelectQuery = function (query) {
         return query.toUpperCase().startsWith("SELECT");
     }
 
@@ -1042,7 +1172,7 @@ var queryForm = new function() {
      * @param {HTMLElement} form, the query form.
      * @return {HTMLElement} the form without the paging input.
      */
-    this.removePagingField = function(form) {
+    this.removePagingField = function (form) {
         $(form.P).remove();
         return form;
     }
@@ -1052,9 +1182,9 @@ var queryForm = new function() {
 /**
  * Small module containing only a converter from markdown to html.
  */
-this.markdown = new function() {
+this.markdown = new function () {
     this.dependencies = ["showdown", "caosdb_utils"];
-    this.init = function() {
+    this.init = function () {
         this.converter = new showdown.Converter();
     };
 
@@ -1065,7 +1195,7 @@ this.markdown = new function() {
      * @param {HTMLElement} textElement - an element with text which is to be
      * converted to html.
      */
-    this.toHtml = function(textElement) {
+    this.toHtml = function (textElement) {
         let text = $(textElement).html();
         let html = this.textToHtml(text);
         $(textElement).html(html);
@@ -1074,28 +1204,28 @@ this.markdown = new function() {
     /**
      * Convert a markdown text to HTML
      */
-    this.textToHtml = function(text) {
+    this.textToHtml = function (text) {
         caosdb_utils.assert_string(text, "param `text`");
         return this.converter.makeHtml(text.trim());
     };
 
-    $(document).ready(function() {
+    $(document).ready(function () {
         caosdb_modules.register(markdown);
     });
 }
 
-var hintMessages = new function() {
-    this.init = function() {
+var hintMessages = new function () {
+    this.init = function () {
         for (var entity of $('.caosdb-entity-panel')) {
             this.hintMessages(entity);
         }
     }
 
-    this.removeMessages = function(entity) {
+    this.removeMessages = function (entity) {
         $(entity).find(".alert").remove();
     }
 
-    this.unhintMessages = function(entity) {
+    this.unhintMessages = function (entity) {
         $(entity).find(".caosdb-f-message-badge").remove();
         $(entity).find(".alert").show();
     }
@@ -1115,7 +1245,7 @@ var hintMessages = new function() {
      * @param {HTMLElement} entity - the element where to replace the
      *     messages.
      */
-    this.hintMessages = function(entity) {
+    this.hintMessages = function (entity) {
 
         // TODO refactor such that the function can detect whether a message is
         // replaced yet instead of "unhintMessage"ing all of them first and do
@@ -1131,7 +1261,7 @@ var hintMessages = new function() {
         for (let alrt in messageType) {
 
             // find all message divs
-            $(entity).find(".alert.alert-" + alrt).each(function(index) {
+            $(entity).find(".alert.alert-" + alrt).each(function (index) {
                 var messageElem = $(this);
 
                 // this way only one badge is shown, even if there are more
@@ -1140,7 +1270,7 @@ var hintMessages = new function() {
 
                     // TODO why is the message badge added to the .caosdb-v-property-row here? shouldn't .caosdb-messages suffice?
                     messageElem.parent('.caosdb-messages, .caosdb-v-property-row').prepend('<button title="Click here to show the ' + messageType[alrt] + ' messages of the last transaction." class="btn caosdb-v-message-badge caosdb-f-message-badge badge alert-' + alrt + '">' + messageType[alrt] + '</button>');
-                    messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function(e) {
+                    messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function (e) {
 
                         // TODO use remove here instead of hide?
                         $(this).hide();
@@ -1235,7 +1365,7 @@ function postXml(xml, basepath, querySegment, timeout) {
         dataType: 'xml',
         timeout: timeout,
         statusCode: {
-            401: function() {
+            401: function () {
                 throw new Error("unauthorized");
             },
         },
@@ -1273,7 +1403,7 @@ async function load_config(filename) {
         } else if (error.status == 404) {
             return [];
         } else {
-            throw new Error("loading '"+ uri + "' failed.", error);
+            throw new Error("loading '" + uri + "' failed.", error);
         }
     }
     return data;
@@ -1391,13 +1521,13 @@ function initOnDocumentReady() {
     }
 
     // show image 100% width
-    $(".entity-image-preview").click(function() {
+    $(".entity-image-preview").click(function () {
         $(this).css('width', '100%');
         $(this).css('max-width', "");
         $(this).css('max-height', "");
     });
 
-    if(typeof caosdb_modules.auto_init === "undefined") {
+    if (typeof caosdb_modules.auto_init === "undefined") {
         // the test index.html sets this to false, 
         // unset -> no tests
         caosdb_modules.auto_init = true;
@@ -1429,12 +1559,12 @@ class _CaosDBModules {
      * @throws TypeError - if module has no `init` method.
      */
     register(module) {
-        if(!(typeof module.init === "function")) {
+        if (!(typeof module.init === "function")) {
             throw new TypeError("modules must define an init function");
         }
 
         this.modules.push(module);
-        if(this.auto_init) {
+        if (this.auto_init) {
             this._init_module(module);
         }
     }
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 1f8db45c07fef49fbd1e90be803d48fc8c96c7d2..239758161b8419a8a5e15799c67c909977f1fbf0 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -1874,6 +1874,25 @@ QUnit.test("annotation module", function(assert) {
 
 /* MODULE navbar */
 QUnit.module("webcaosdb.js - navbar", {
+    before: () => {
+        $(document.body).append('<div id="top-navbar"><ul class="caosdb-navbar"/></div>');
+    },
+    beforeEach: () => {
+        $(".caosdb-f-navbar-toolbox").remove();
+    },
+    after: () => {
+        $("#top-navbar").remove();
+    },
+});
+
+QUnit.test("get_navbar", function(assert) {
+    assert.equal(navbar.get_navbar().className, "caosdb-navbar");
+});
+
+QUnit.test("add_button wrong parameters", function(assert) {
+    assert.throws(()=>{navbar.add_button(undefined)}, /button is expected/, "undefined throws");
+    assert.throws(()=>{navbar.add_button({"test": "an object"})}, "object throws");
+    assert.throws(()=>{navbar.add_button(["array of strings"])}, "array of string throws");
 });
 
 QUnit.test("test button classes", function(assert) {
@@ -1883,3 +1902,40 @@ QUnit.test("test button classes", function(assert) {
     assert.ok(result.hasClass("btn-link"), "has class btn-link");
     assert.equal(result.text(), "TestButton", "text is correct");
 });
+
+QUnit.test("add_tool", function(assert) {
+    assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox");
+    navbar.add_tool("TestButton", "TestMenu");
+
+    var toolbox = $("ul.caosdb-f-navbar-toolbox");
+    assert.equal(toolbox.length, 1, "new toolbox");
+    assert.equal(toolbox.find("button").length, 1, "new button");
+    assert.equal(toolbox.find("button").text(), "TestButton", "Name correct")
+
+    assert.notOk(toolbox.hasClass("btn"));
+    assert.notOk(toolbox.hasClass("btn-link"));
+    assert.notOk(toolbox.hasClass("navbar-btn"));
+
+    assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn"));
+    assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn-link"));
+    assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("navbar-btn"));
+});
+
+QUnit.test("toolbox example", function(assert) {
+    // this is a kind of integration test and it uses the toolbox_example
+    // module from toolbox_example.js. That example is also usefull for manual
+    // testing.
+    assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox");
+    toolbox_example.init();
+    assert.equal($(".caosdb-f-navbar-toolbox").length, 3, "three toolboxes");
+
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"]').length, 1, "one 'Useful Links' toolbox");
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"] a[href="https://indiscale.com"]').length, 1, "one external link");
+
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"]').length, 1, "one 'Server-side Scripts' toolbox");
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form input[type="submit"]').attr("value"), "Trigger Crawler", "one crawler trigger button");
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form').attr("title"), "Trigger the crawler.", "form has tooltip");
+
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox");
+    assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons");
+});
diff --git a/test/ext/js/toolbox_example.js b/test/ext/js/toolbox_example.js
new file mode 100644
index 0000000000000000000000000000000000000000..9b9ef6548353c248376c3c3183835d03698366da
--- /dev/null
+++ b/test/ext/js/toolbox_example.js
@@ -0,0 +1,49 @@
+var toolbox_example = function() {
+
+    var init = function() {
+        navbar.add_button("Single Button", {callback: ()=>{alert("Single Button");}, title: "Click me!"});
+        navbar.add_tool("Tool 1", "Tools", {callback: ()=>{alert("Tool 1");}, title: "Tooltip 1"});
+        navbar.add_tool("Tool 2", "Tools", {callback: ()=>{alert("Tool 2");}, title: "Tooltip 2"});
+        navbar.add_tool("Tool 3", "Tools", {callback: ()=>{alert("Tool 3");}, title: "Tooltip 3"});
+
+        navbar.add_tool($('<a href="https://indiscale.com">Link1</a>')[0], "Useful Links", {title: "Browse to indiscale.com"});
+
+
+        const script = "crawler.py"
+        const args = {
+            "-p0": "positional argument 1",
+            "-p1": "positional argument 2",
+            "-Ooption1": "option value 1",
+            "-Ooption2": "option value 2",
+        };
+        const button_name = "Trigger Crawler";
+        const title = "Trigger the crawler.";
+
+        const crawler_form = make_scripting_caller_form(
+            script, args, button_name);
+
+        navbar.add_tool(crawler_form, "Server-side Scripts", {title: title});
+    }
+
+    var make_scripting_caller_form = function (script, args, button_name) {
+        const scripting_caller = $(`
+        <form method="POST" action="/scripting">
+          <input type="hidden" name="call" value="${script}"/>
+          <input type="submit"
+              class="btn btn-link" value="${button_name}"/>
+        </form>`);
+
+        // add arguements
+        for (const arg in args) {
+            scripting_caller.append(`<input type="hidden" name="${arg}"
+                value="${args[arg]}"/>`);
+        }
+
+        return scripting_caller[0];
+    }
+
+    return {
+        init: init
+    };
+
+}();