diff --git a/CHANGELOG.md b/CHANGELOG.md
index a50eb436599300354f5b26ec853f3940a313ac0a..c295a5fc637112645189759f3267859385514518 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   See `build.properties.d/00_default.properties` for more information
 * Start editing an entity/creating a new record directly by adding an `#edit` or
   `#new_record` URI fragment, respectively.
+* Optional WYSIWYG editor with markdown output for text properties. Controled by
+  the `BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT` build variable which is set do
+  `DISABLED` by default.
 
 ### Changed (for changes in existing functionality)
 
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index c6043d09d049b91cfc0a03560970537feb8bbc06..f3e1d1706dd8dd9bdbdab16377c2c75ed0299f7f 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -24,5 +24,12 @@
 * proj4js-2.5.0
 * Proj4Leaflet-1.0.1
 
+## For CKEditor (WYSIWYG editor in edit mode)
+
+* we're using a custom-built ckeditor 31.0.0 from
+  https://ckeditor.com/ckeditor-5/online-builder/ with a customized set of
+  editor plugins. Please refer to the `package.json` within
+  `libs/ckeditor...zip` for a full list of said plugins.
+
 ## For testing
 * qunit-2.9.2
diff --git a/Makefile b/Makefile
index c820cfb159d9b63518dff445883615e67fbd9289..5cca686319c617006007e4884cfb269796ac7af4 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
 TEST_SSS_DIR =$(abspath test/server_side_scripting)
-LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js
+LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js js/ckeditor.js
 
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
@@ -305,6 +305,9 @@ $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js
 $(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@
 
+$(LIBS_DIR)/js/ckeditor.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/ckeditor5-31.0.0-k356w86hp13l/build/ckeditor.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index db01885d1494ffd3f07eecfa2e6f2d26f56e3705..535a6c846a5c39c48937dee43f087501f43285ae 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -60,6 +60,8 @@ BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
 BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
 BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=person_reference
 
+BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT=DISABLED
+
 ##############################################################################
 # Navbar properties
 ##############################################################################
@@ -173,4 +175,6 @@ MODULE_DEPENDENCIES=(
     qrcode.js
     ext_qrcode.js
     form_panel.js
+    ckeditor.js
+    ext_editmode_wysiwyg_text.js
 )
diff --git a/libs/ckeditor5-31.0.0-k356w86hp13l.zip b/libs/ckeditor5-31.0.0-k356w86hp13l.zip
new file mode 100644
index 0000000000000000000000000000000000000000..71523482a2bdfa7c0a9776eeed7aa7be010e053b
Binary files /dev/null and b/libs/ckeditor5-31.0.0-k356w86hp13l.zip differ
diff --git a/src/core/js/ext_editmode_wysiwyg_text.js b/src/core/js/ext_editmode_wysiwyg_text.js
new file mode 100644
index 0000000000000000000000000000000000000000..a95b9911e37bfd2cff7022a624b64942b10655d8
--- /dev/null
+++ b/src/core/js/ext_editmode_wysiwyg_text.js
@@ -0,0 +1,118 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@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/>.
+ */
+
+"use strict";
+
+/**
+ * Replaces textareas in the edit mode by a wysiwyg editor
+ *
+ * @module ext_editmode_wysiwyg_text
+ * @version 0.1
+ *
+ * @param jQuery - well-known library.
+ * @param log - singleton from loglevel library or javascript console.
+ * @param {class} ClassicEditor - ClassicEditor class from ckEditor
+ * @param {module} edit_mode - caosdb's edit-mode module
+ * @param {function} getPropertyElements - caosdb's function to extract the
+ *     property HTML elements from an entity HTML element
+ * @param {function} getPropertyDatatype - caosdb's function to extract the
+ *     data type from a property HTML element
+ * @param {function} getPropertyName - caosdb's function to extract the
+ *     name from a property HTML element
+ */
+var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, getPropertyElements, getPropertyDatatype, getPropertyName) {
+
+    var insertEditorInProperty = async function (prop) {
+        if (!(getPropertyDatatype(prop) === 'TEXT')) {
+            // Ignore anything that isn't a list property, even LIST<TEXT>
+            return;
+        }
+
+        try{
+            const editor = await ClassicEditor
+                .create(prop.querySelector('textarea'), {
+                    // use all plugins since we built the editor dependency to
+                    // contain only those we need.
+                    plugins: ClassicEditor.builtinPlugins,
+                    // Markdown needs a header row so enforce this
+                    table: {
+                        defaultHeadings: {
+                            rows: 1,
+                            columns: 0
+                        },
+                    },
+                })
+            console.log('Initialized editor for ' + getPropertyName(prop));
+            // Manually implement saving the data since edit mode is not
+            // a form to be submitted.
+            editor.model.document.on("change:data", (e) => {
+                editor.updateSourceElement();
+            });
+        } catch(error) {
+            console.error(error.stack);
+        }
+    }
+
+    var replaceTextAreas = function (entity) {
+        const properties = getPropertyElements(entity);
+        for (let prop of properties) {
+            // TODO(fspreck): This will be replaced by a whitelist of properties
+            // in the future.
+            if (getPropertyName(prop)) {
+                insertEditorInProperty(prop);
+            }
+        }
+    }
+
+    var init = function () {
+
+        // Insert an editor into all TEXT properties of the entity which is
+        // being edited.
+        document.body.addEventListener(edit_mode.start_edit.type, (e) => {
+            console.log('Replacing text areas ...');
+            ext_editmode_wysiwyg_text.replaceTextAreas(e.target);
+        }, true);
+
+        // Listen to added properties and replace the textarea if necessary
+        document.body.addEventListener(edit_mode.property_added.type, (e) => {
+            console.log('Replacing textarea in ' + getPropertyName(e.target));
+            ext_editmode_wysiwyg_text.insertEditorInProperty(e.target);
+        }, true)
+
+        // Listen to properties, the data type of which has changed. Mainly
+        // because of change from list to scalar and vice versa.
+        document.body.addEventListener(edit_mode.property_data_type_changed.type, (e) => {
+            console.log('Re-rendering ' + getPropertyName(e.target));
+            ext_editmode_wysiwyg_text.insertEditorInProperty(e.target);
+        }, true);
+    };
+
+    return {
+        init: init,
+        replaceTextAreas: replaceTextAreas,
+        insertEditorInProperty: insertEditorInProperty,
+    };
+}($, log.getLogger("ext_editmode_wysiwyg_text"), ClassicEditor, edit_mode, getPropertyElements, getPropertyDatatype, getPropertyName);
+
+$(document).ready(() => {
+    if ("${BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT}" == "ENABLED") {
+        caosdb_modules.register(ext_editmode_wysiwyg_text);
+    }
+});