diff --git a/src/gapfill-open.js b/src/gapfill-open.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b3b3662b8333e54a25a675b9467b7e01b49b981
--- /dev/null
+++ b/src/gapfill-open.js
@@ -0,0 +1,63 @@
+/**
+ * Custom SurveyJS question type for a "fill-in-the-gaps" text,
+ * with an open-ended text input for each gap
+ */
+export const gapfillOpenWidget = {
+    name: "gapfill-open",
+    title: "Gap-Fill Text (Open)",
+    /**
+     * This function should return true when the widget and all needed resources
+     * are loaded
+     */
+    widgetIsLoaded: function () {
+        return true;
+    },
+    /**
+     * This function should return true if the widget should be applied to the question */
+    isFit: function (question) {
+        return question.getType() === this.name;
+    },
+    init() {
+        //Register a new type using the empty question as the base.
+        Survey.Serializer.addClass(this.name, [], null, "empty");
+    },
+    /** Static HTML template rendered by SurveyJS */
+    htmlTemplate: '<p id="gapfill-container"><template id="template-gap"><input type="text" class="sd-input inline-input"/></template></p>',
+    /**
+     * Function called after the HTML template is rendered. This time we actually have the `question` object
+     * and the `el` element, to build the question according to the JSON
+     */
+    afterRender: function (question, el) {
+        // The gap-fill text is made of segments, which are either plain pieces
+        // of text (strings) or "gaps" (objects).
+        // We append these to build the text, turning strings into <span>s and
+        // gaps into <input> text fields
+        let nbGaps = 0;
+        const segmentElems = new DocumentFragment(); // a bit faster than mutating the DOM all the time
+        const gapTemplate = document.getElementById("template-gap").content.firstChild;
+        for (const segment of question.jsonObj.segments) {
+            let segmentElem;
+            if (typeof segment === 'string' || segment instanceof String) {
+                segmentElem = document.createElement("span");
+                segmentElem.innerText = segment;
+            } else {
+                // It's a gap
+                // Create the <input> element
+                segmentElem = gapTemplate.cloneNode(true);
+                segmentElem.setAttribute("data-index", nbGaps); // The node knows its index
+                // Add listener to update the question's value when the input value changes
+                segmentElem.addEventListener("change", (e) => {
+                    // The input node knows its index, therefore is able to update the question value at the correct index
+                    question.value[parseInt(e.target.getAttribute("data-index"))] = e.target.value;
+                });
+                nbGaps++;
+            }
+            // Add segment
+            segmentElems.appendChild(segmentElem);
+        }
+        // Initialize question value array
+        question.value = new Array(nbGaps);
+        // Finally add everything to the DOM
+        el.appendChild(segmentElems);
+    },
+};
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 5de1494f5823abf7611e8130f9166db1b18d642d..7cacf5f0b056d72801c2035700d980121d3daa36 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,11 +1,13 @@
 import {json} from "./json.js"
 import {gapfillSelectWidget} from "./gapfill-select.js";
+import {gapfillOpenWidget} from "./gapfill-open.js";
 
 window.addEventListener('load', main)
 
 function main() {
     // Register our custom question types
     Survey.CustomWidgetCollection.Instance.add(gapfillSelectWidget, gapfillSelectWidget.name);
+    Survey.CustomWidgetCollection.Instance.add(gapfillOpenWidget, gapfillOpenWidget.name);
 
     let survey = new Survey.Model(json);
 
diff --git a/src/json.js b/src/json.js
index a1ed56016ae7637c9223f339892e11c7c165a013..7b3a22e272c64c8324de926ff70dfc8e85911b26 100644
--- a/src/json.js
+++ b/src/json.js
@@ -2,7 +2,7 @@ export const json = {
     elements: [
         {
             type: "gapfill-select",
-            name: "pg20",
+            name: "q1",
             title: "The greatest song in the world",
             segments: [
                 "🎶\nWe're no strangers to ",
@@ -37,6 +37,16 @@ export const json = {
                 "run around",
                 "desert"
             ]
+        },
+        {
+            type: "gapfill-open",
+            name: "q2",
+            title: "Open-ended gapfill",
+            segments: [
+                "Lorem ",
+                {},
+                " dolor sit amet [...]"
+            ], correctAnswer: ["ipsum"]
         }
     ]
 };
\ No newline at end of file
diff --git a/src/style.css b/src/style.css
index d201ece86408fc53de2ff95d4ac18c594cd26cf8..b9f9dfd699ad5b35c9a79e3d7355d2863cb4618d 100644
--- a/src/style.css
+++ b/src/style.css
@@ -8,7 +8,7 @@ p#gapfill-container {
     white-space: pre-wrap;
     line-height: 3em;
 }
-select.sd-dropdown.inline-dropdown {
+select.sd-dropdown.inline-dropdown, input.sd-input.inline-input {
     display: inline-block;
     width: fit-content;
     padding: 8px;