diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6a7a87c4c3f83dac53c4a3ba21a983f9c551bbb..04ae51995a9f3f7ec21a4b6a87b9e10ed193f8d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added (for new features, dependecies etc.)
 
+* `ext_applicable` module for building fancy features which append
+  functionality to entities (EXPERIMENTAL).
 * `ext_cosmetics` module which converts http(s) uris in property values into
   clickable links.
 * Displaying and interacting with the entity state.
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 76ce6c7fa0e2f4af1babf6dc3661fe86e0a7bf18..58d20c521100bbd43e094c2da402abc38ea555bc 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -127,6 +127,7 @@ MODULE_DEPENDENCIES=(
     ext_autocomplete.js
     preview.js
     ext_references.js
+    ext_applicable.js
     ext_table_preview.js
     ext_xls_download.js
     query_shortcuts.js
diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css
index e5cd26401be57fceded902b156aa590f1f9d0a34..b76a5fe9ecc9f6d866dc159429ef7401ac3ddd5d 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -494,7 +494,7 @@ h5 {
     margin-top: auto;
     margin-bottom: auto;
     margin-right: 8px;
-    height: 30px;
+    max-height: 30px;
 }
 
 .caosdb-comment-action-item {
diff --git a/src/core/css/webcaosdb.less b/src/core/css/webcaosdb.less
deleted file mode 100644
index 54e9113df34232234ed6814a4383cf7ba954e121..0000000000000000000000000000000000000000
--- a/src/core/css/webcaosdb.less
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * ** header v3.0
- * This file is a part of the CaosDB Project.
- *
- * Copyright (C) 2018 Research Group Biomedical Physics,
- * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * ** end header
- */
-
-@radius_normal: 4px;
-@margin_normal: 8px;
-@border1: 1px solid #e7e7e7;
-
-.caosdb-v-show-only-child {
-    display: none;
-}
-
-.caosdb-v-show-only-child:only-child {
-    display: initial;
-}
-
-.caosdb-comment-annotation-text h1 {
-    font-size: 24px;
-}
-
-.caosdb-comment-annotation-text h2 {
-    font-size: 20px;
-}
-
-.caosdb-comment-annotation-text h3 {
-    font-size: 18px;
-}
-
-.caosdb-comment-annotation-text h4 {
-    font-size: 16px;
-}
-
-.caosdb-show-preview-button, .caosdb-hide-preview-button {
-    position: absolute;
-    top: 5px;
-    left: -5px;
-}
-
-.caosdb-entity-heading-attr {
-    overflow-x: auto;
-}
-
-a.label.caosdb-id-button:hover {
-    background-color: #5E6762;
-}
-
-.caosdb-entity-preview .caosdb-entity-panel-body {
-    overflow-y: auto;
-    max-height: 250px;
-}
-
-.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:first-child {
-    margin-left: 34px;
-}
-.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:last-child {
-    margin-right: 34px;
-}
-
-.caosdb-preview-carousel-nav {
-    position: relative;
-}
-.caosdb-query-panel {
-	padding-top: 20px;
-	padding-bottom: 20px;
-}
-
-.navbar-light .btn-link:hover {
-	text-decoration: none;
-}
-
-.navbar-light .btn-link:focus {
-    text-decoration: none;
-}
-
-.caosdb-property-name {
-	font-weight: bold;
-	margin-left: @margin-normal;
-}
-
-.caosdb-square {
-	position: relative;
-	overflow: hidden;
-}
-
-.caosdb-square::before {
-	content: "";
-	display: block;
-	padding-top: 100%;
-}
-
-.caosdb-square-content {
-	position: absolute;
-	top: 0px;
-	right: 0px;
-	bottom: 0px;
-	left: 0px;
-}
-
-.caosdb-parent-name {
-    font-weight: bold;
-    margin-right: 4px;	
-}
-
-/* lists of values */
-/* INLINE (with default scroll-bar) */
-.caosdb-value-list {	
-	overflow-x: auto;
-}
-
-.caosdb-value-list > .btn-group > .btn {
-    float: none;
-}
-.caosdb-value-list > .btn-group,
-.caosdb-value-list > ol {
-	display: inline-block;
-    float: none;
-    white-space: nowrap;
-    margin-bottom: 0px;
-    margin-left: 0px;
-}
-
-.caosdb-value-list > .list-inline > li {
-    border-left-width: 0px; 
-}
-
-.caosdb-value-list > .list-inline > li:first-child {
-	border-radius: @radius_normal 0px 0px @radius_normal;
-	border-left-width: 1px;
-}
-
-.caosdb-value-list > .list-inline > li:last-child {
-    border-radius: 0px @radius_normal @radius_normal 0px;
-}
-
-/* single boolean values */
-.caosdb-boolean-true {
-	font-weight: bold;
-	font-size: 90%;
-	border: 1px solid #bbb;
-	padding: 2px 8px;
-	border-radius: 8px;
-}
-
-.caosdb-boolean-false {
-    .caosdb-boolean-true();
-}
-
-.caosdb-entity-panel-heading {
-    border-bottom-left-radius: 3px;
-    border-bottom-right-radius: 3px;  
-}
-
-.caosdb-entity-panel-body {
-    padding: 0px 15px;	
-}
-
-.caosdb-entity-panel-body > :first-child {
-	margin-top: 15px;
-}
-
-.caosdb-entity-heading-attr-name {
-	color: #6c6c6c;
-    font-size: 90%;
-    margin-right: 0.3em; 
-}
-
-.caosdb-subproperty-divider {
-    margin: 4px 0px;
-    border-top: 1px solid #dddddd
-}
-
-.caosdb-prop-label {
-    background-color: #f5f5f5;
-    font-weight: bold;
-    padding: 8px 15px;
-}
-
-.caosdb-prop-value {
-    background-color: #ffffff;
-    padding: 8px 15px;
-}
-
-.caosdb-prop-list-group .row {
-    margin-left: 0px;
-    margin-right: 0px;
-}
-
-.caosdb-prop-list-group>.list-group-item {
-    padding: 0px
-}
-
-.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-label
-    {
-    border-top-left-radius: @radius_normal;
-    border-top-right-radius: @radius_normal;
-}
-
-.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-value
-    {
-    border-top-right-radius: @radius_normal;
-}
-
-.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-label {
-    border-bottom-left-radius: @radius_normal;
-}
-
-.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-value {
-    border-bottom-left-radius: @radius_normal;
-    border-bottom-right-radius: @radius_normal;
-}
-
-.caosdb-label-record {
-    background-color: #F92108;
-    margin-right: @margin-normal;
-}
-
-.caosdb-label-recordtype {
-    background-color: #00A32E;
-    margin-right: @margin-normal;
-}
-
-.caosdb-label-property {
-    background-color: #496DAB;
-    margin-right: @margin-normal;
-}
-
-.caosdb-label-file {
-    background-color: #C92E86;
-    margin-right: @margin-normal;
-}
-
-.label.caosdb-id-button {
-    background-color: #4E5752;
-}
-
-
-.caosdb-properties-heading {
-    padding-top: 2px;
-    padding-bottom: 2px;
-    background-color: #f5f5f5;
-    color: #7c7c7c
-}
-
-.caosdb-parents-heading {
-    .caosdb-properties-heading();
-}
-
-.caosdb-comments-heading {
-    .caosdb-properties-heading();
-}
-
-.caosdb-parent-item {
-    padding-left: 40px;
-    text-indent: -40px
-}
-
-.caosdb-unit {
-    color: #8c8c8c;
-    font-size: 80%;
-    margin-left: 0.3em;
-}
-
-.navbar-brand {
-    display: flex;
-    align-items: center;
-}
-
-.navbar-brand>img {
-    padding: 0px 0px;
-    height: 100%
-}
-
-.caosdb-fs-cwd::before {
-    content: " > ";
-}
-
-.caosdb-fs-dir>.glyphicon::before {
-    content: "\e117";
-}
-
-.caosdb-fs-dir>.glyphicon {
-    margin-right: @margin-normal;
-}
-
-.caosdb-fs-dir:hover>.glyphicon::before {
-    content: "\e118";
-}
-
-.caosdb-fs-file>.glyphicon::before {
-    content: "\e022";
-}
-
-.caosdb-fs-file>.glyphicon {
-    margin-right: @margin-normal;
-}
-
-.caosdb-fs-file:hover>.glyphicon::before {
-    content: "\e025";
-}
-
-.caosdb-fs-btn-file {
-    padding: 0px;
-    background-color: transparent;
-    border: 0px;
-}
-
-.caosdb-fs-btn-file:hover .caosdb-label-file {
-    background-color: #F96EB6;
-}
-
-.caosdb-fs-btn-file:hover .caosdb-label-id {
-    background-color: #6E8782;
-}
-
-.back-to-top {
-    cursor: pointer;
-    position: fixed;
-    bottom: 10px;
-    right: 15px;
-    display: none;
-    z-index: 1050;
-}
-
-.caosdb-logo {
-    margin-right: @margin-normal;
-}
-
-.caosdb-comment-action-item {
-    padding-left: 4px;
-    padding-right: 4px;
-    border-left: 1px solid #7c7c7c;
-}
-
-.caosdb-comment-action>.caosdb-comment-action-item:first-child {
-    border-left: 0px solid #7c7c7c;
-}
-
-.caosdb-pagination {
-    margin: 5px 15px;
-}
-
-.caosdb-pagination-navbar {
-    padding-bottom: 5px;
-    position: fixed;
-    bottom: 0px;
-    width: 100%;
-    border: 0px;
-    border-top: @border1;
-    z-index: 1000;
-    position: fixed;
-}
-
-.caosdb-heading {
-    color: #5e5e5e;
-    background-color: #f8f8f8;
-    border-bottom: @border1;
-}
-
-.caosdb-heading>.container {
-    padding: 20px 0px;
-}
-
-.spinning {
-    animation: spin 2s linear infinite;
-}
-
-@keyframes spin {
-    0% { transform: rotate(0deg); }
-    100% { transform: rotate(360deg); }
-}
-
-.flipped-horiz-icon {
-    transform: scaleX(-1);
-}
-
-.spacer {
-    margin-left: 12px;
-}
-
-input[type="file"] {
-    display: none;
-}
diff --git a/src/core/js/ext_applicable.js b/src/core/js/ext_applicable.js
new file mode 100644
index 0000000000000000000000000000000000000000..6e4ae94f742e00e46f6cff609f4217100c9f4292
--- /dev/null
+++ b/src/core/js/ext_applicable.js
@@ -0,0 +1,342 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+/**
+ * Useful helpers for is_applicable functionality.
+ */
+var _helpers = function(getEntityPath) {
+
+  /**
+   * Check if an entity has a path attribute and one of a set of extensions.
+   *
+   * Note: the array of extensions must contain only lower-case strings.
+   *
+   * @param {HTMLElement} entity
+   * @param {string[]} extensions - an array of file extesions, e.g. `jpg`.
+   * @return {boolean} true iff the entity has a path with one of the
+   *     extensionss.
+   */
+  const path_has_file_extension = function(entity, extensions) {
+      const path = getEntityPath(entity);
+      if (path) {
+          for (var ext of extensions) {
+              if (path.toLowerCase().endsWith(ext)) {
+                  return true;
+              }
+          }
+      }
+      return false;
+  }
+
+  return {
+    path_has_file_extension: path_has_file_extension
+  };
+}(getEntityPath)
+
+var ext_applicable = function($, logger, is_in_view_port, load_config, getEntityPath, connection, helpers, createWaitingNotification) {
+
+  const version = "0.1";
+
+  /**
+   * Run through all creators and call the "create" function of the first
+   * creator which returns true from is_applicable(entity).
+   *
+   * @param {HTMLElement} entity
+   * @param {Creator[]} creators - list of creators
+   * @return {HTMLElement|String} content - the result of the first matching
+   *     creator.
+   */
+  const root_creator = async function (entity, creators) {
+    for (let c of creators) {
+      var is_applicable = false;
+      try {
+        is_applicable = await c.is_applicable(entity);
+      } catch (err) {
+        logger.error(`error in is_applicable function of creator`, c, err);
+        continue;
+      }
+      if (is_applicable) {
+        const content = await c.create(entity);
+        return content;
+      }
+    }
+    return undefined;
+  }
+
+  var _set_content = function (container, content) {
+    const _container = $(container);
+    _container.empty();
+
+    if (content) {
+        _container.append(content);
+    }
+  }
+
+  /**
+   * Replace the old content (if any) of the container by the new the content
+   * (created by the creators).
+   *
+   * Dispatch contentReadyEvent or noContentEvent if given.
+   *
+   * @param {HTMLElement} container
+   * @param {HTMLElement|string} [content]
+   * @param {Event} [contentReadyEvent]
+   * @param {Event} [noContentEvent]
+   */
+  const set_content = async function (container, content, contentReadyEvent, noContentEvent) {
+    try {
+      const wait = createWaitingNotification("Please wait...");
+      _set_content(container, wait);
+      const result = await content;
+      _set_content(container, result);
+      if (result && contentReadyEvent) {
+        container.dispatchEvent(contentReadyEvent);
+      } else if (!result && noContentEvent) {
+        container.dispatchEvent(noContentEvent);
+      }
+
+    } catch (err) {
+      logger.error(err);
+      const err_msg = "An error occured while loading this content.";
+      _set_content(container, err_msg);
+    }
+  }
+
+
+  /**
+   * @param {HTMLElement} entity
+   * @param {get_container_cb} get_container_cb
+   * @param {Creator[]} creators
+   * @param {Event} [contentReadyEvent]
+   * @param {Event} [noContentEvent]
+   */
+  const _root_handler = function (entity, get_container_cb, creators, contentReadyEvent, noContentEvent) {
+    const _container = get_container_cb(entity);
+    if (_container) {
+      const content = root_creator(entity, creators);
+      set_content(_container, content, contentReadyEvent, noContentEvent);
+    }
+  }
+
+
+  /**
+   * The root handler trigger call the root_handler callback on every
+   * .caosdb-entity-panel and every .caosdb-entity-preview in the viewport.
+   *
+   * @param {function} root_handler - the root handler callback.
+   */
+  var root_handler_trigger = function(root_handler) {
+    var entities = $(".caosdb-entity-panel,.caosdb-entity-preview");
+    for (let entity of entities) {
+
+      // TODO viewport + 1000 px for earlier loading
+      if (is_in_view_port(entity)) {
+        root_handler(entity);
+      }
+    }
+  }
+
+
+  /**
+   * Initialize the scroll watcher which listens on the scroll event of the
+   * window and triggers the root handler with a delay after the last
+   * scroll event.
+   *
+   * @param {integer} delay - timeout in milliseconds after the last scroll
+   *     event. After this timeout the trigger is called.
+   * @param {function} trigger - the root handler callback which is called.
+   */
+  var init_watcher = function(delay, trigger) {
+    var scroll_timeout = undefined;
+    $(window).scroll(() => {
+        if (scroll_timeout) {
+            clearTimeout(scroll_timeout);
+        }
+        scroll_timeout = setTimeout(trigger, delay);
+    });
+
+    var preview_timeout = undefined;
+
+    // init watcher on newly loaded entity previews.
+    window.addEventListener(
+      preview.previewReadyEvent.type,
+      () => {
+        if (preview_timeout) {
+          clearTimeout(preview_timeout);
+        }
+        preview_timeout = setTimeout(trigger, delay);
+      },
+      true);
+
+    // trigger for the first time
+    trigger();
+  };
+
+  /**
+   * @callback get_container_cb
+   * @param {HTMLElement} entity - the entity for which this callback shall
+   *     return the is_applicable container.
+   * @returns {HTMLElement} child of entity which is a container for the
+   *     is_applicable_app
+   */
+
+  /**
+   * @type {IsApplicableApp}
+   * @property {IsApplicableConfig} config
+   * @property {Creator[]} creators
+   * @property {function} root_handler
+   */
+
+  /**
+   * @type {IsApplicableConfig}
+   * @property {string|HTMLElement|function} fallback - Fallback content or
+   *     callback if none of the creators are applicable.
+   * @property {string} version - the version of the configuration which must
+   *     match this module's version.
+   * @property {CreatorConfig[]} creators - an array of creators.
+   */
+
+  /**
+   * make a fallback creator
+   *
+   * @return {Creator}
+   */
+  const _make_fallback_creator = function(fallback) {
+    if (fallback) {
+      return {
+        id: "_generated_fallback_creator",
+        is_applicable: (entity) => true, // always applicable
+        create: typeof fallback === "function" ? fallback : (entity) => fallback,
+      };
+    }
+    return undefined;
+  }
+
+  const _make_creator = function (c) {
+    return {
+      id: c.id,
+      is_applicable: typeof c.is_applicable === "function" ?
+                         c.is_applicable : eval(c.is_applicable),
+      create: typeof c.create === "function" ? c.create : eval(c.create)
+    };
+  }
+
+
+  /**
+   * @param {IsApplicableConfig}
+   * @return {Creator[]} creators
+   */
+  const _make_creators = function(config) {
+    const creators = [];
+    for (let c of config.creators) {
+      creators.push(_make_creator(c));
+    }
+    const fallback_creator = _make_fallback_creator(config.fallback);
+    if (fallback_creator) {
+      creators.push(fallback_creator);
+    }
+    return creators;
+  }
+
+  /**
+   * @type {CreatorConfig}
+   * @property {string} [id] - a unique id for the creator. optional, for
+   *     debuggin purposes.
+   * @property {function|string} is_applicable - If this is a string this has
+   *     to be valid javascript! An asynchronous function which accepts one
+   *     parameter, an entity in html representation, and which returns true
+   *     iff this creator is applicable for the given entity.
+   * @property {string} create - This has to be valid javascript! An
+   *     asynchronous function which accepts one parameter, an entity in html
+   *     representation. It returns a HTMLElement or text node which will be
+   *     shown in the bottom line container iff the creator is applicable.
+   */
+
+  /**
+   * @param {string} app_name - the name of this app.
+   * @param {get_container_cb} get_container_cb
+   * @return {get_container_cb} wrapped function which also checks if the
+   *     container has already been filled with the created content.
+   */
+  const _make_get_container_wrapper = function (get_container_cb, app_name) {
+    const _wrapper = function (entity) {
+      const container = get_container_cb(entity);
+      const app_done = $(container).data(app_name);
+      if(!app_done) {
+        // mark container as used
+        $(container).data(app_name, "done");
+        return container
+      }
+      // don't return the container if already used by this app
+      return undefined;
+    }
+    return _wrapper;
+  }
+
+  /**
+   * @param {string} that_version - a version string.
+   * @throws {Error} if that_version doesn't match this modules version.
+   */
+  const _check_version = function(that_version) {
+    if(that_version != version) {
+      throw new Error(`Wrong version in config. Was '${that_version}', should be '${version}'.`);
+    }
+  }
+
+  /**
+   * @param {string} app_name
+   * @param {IsApplicableConfig} config
+   * @param {get_container_cb} get_container
+   * @param {Event} [contentReadyEvent]
+   * @param {Event} [noContentEvent]
+   * returns {IsApplicableApp}
+   */
+  const create_is_applicable_app = function(app_name, config, get_container, contentReadyEvent, noContentEvent) {
+    logger.debug("create_is_applicable_app", config, get_container);
+    _check_version(config["version"])
+    const creators = _make_creators(config)
+    const get_container_wrapper = _make_get_container_wrapper(get_container, app_name);
+
+    const root_handler = (entity) => _root_handler(entity, get_container_wrapper, creators, contentReadyEvent, noContentEvent);
+
+    if (config.init_watcher) {
+      init_watcher(config.delay || 500, () => {root_handler_trigger(root_handler);});
+    }
+    return {
+      config: config,
+      creators: creators,
+      root_handler: root_handler,
+    }
+  }
+
+  return {
+    create_is_applicable_app: create_is_applicable_app,
+    root_handler_trigger: root_handler_trigger,
+    init_watcher: init_watcher,
+    helpers: helpers,
+    version: version,
+  };
+
+}($, log.getLogger("ext_applicable"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, _helpers, createWaitingNotification);
diff --git a/test/core/js/modules/ext_applicable.js.js b/test/core/js/modules/ext_applicable.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..596f41c9c9a80e47f4a8ebd8268194af4eba1236
--- /dev/null
+++ b/test/core/js/modules/ext_applicable.js.js
@@ -0,0 +1,30 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+QUnit.module("ext_applicable", {});
+
+QUnit.test("availability", function(assert) {
+  assert.equal(ext_applicable.version, "0.1");
+});