diff --git a/backend/app/main.py b/backend/app/main.py
index a5ce2d216da951389d2e33694c51ccfae1a96df1..a474e0ee09c95f98a2674ff37f61536e11cb573f 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -655,7 +655,7 @@ def feedback_message(
     background_tasks.add_task(
         send_websoket_feedback,
         session_id,
-        schemas.MessageFeedback.model_validate(feedback).to_dict()
+        schemas.MessageFeedback.model_validate(feedback).to_dict(),
     )
 
     return feedback.id
diff --git a/backend/app/models.py b/backend/app/models.py
index 0dd3cd97ac1e9110442e08ca6dd1eb6425e2055a..e5a1a147b1e1007e5214fc07b17436b8b0d7e5b9 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -219,3 +219,4 @@ class SurveyResponse(Base):
     question_id = Column(Integer, ForeignKey("survey_questions.id"))
     selected_id = Column(Integer)
     response_time = Column(Float)
+    text = Column(String)
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 578edee0c4abb25b4013cb39fea927ef29e362a7..7c4a8b60eb8ee09aaa93e1ea7622ec4f2142d31c 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -249,6 +249,7 @@ class SurveyResponseCreate(BaseModel):
     question_id: int
     selected_id: int
     response_time: float
+    text: str | None = None
 
 
 class SurveyResponse(BaseModel):
@@ -261,3 +262,4 @@ class SurveyResponse(BaseModel):
     question_id: int
     selected_id: int
     response_time: float
+    text: str | None = None
diff --git a/frontend/src/lib/api/survey.ts b/frontend/src/lib/api/survey.ts
index 8ac70e3b4eae6f900f2af303a3f39232f3a75537..93a27dc2c06701a4ca8e205e47b3621c692dafb3 100644
--- a/frontend/src/lib/api/survey.ts
+++ b/frontend/src/lib/api/survey.ts
@@ -19,7 +19,8 @@ export async function sendSurveyResponseAPI(
 	group_id: number,
 	question_id: number,
 	option_id: number,
-	response_time: number
+	response_time: number,
+	text: string = ''
 ) {
 	const response = await axiosInstance.post(`/surveys/responses`, {
 		uuid,
@@ -28,7 +29,8 @@ export async function sendSurveyResponseAPI(
 		question_id,
 		group_id,
 		selected_id: option_id,
-		response_time
+		response_time,
+		text
 	});
 
 	if (response.status !== 201) {
diff --git a/frontend/src/lib/components/surveys/gapfill.svelte b/frontend/src/lib/components/surveys/gapfill.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..be1cffaafd8b905448e1ae8ca68ca2cd03306e4d
--- /dev/null
+++ b/frontend/src/lib/components/surveys/gapfill.svelte
@@ -0,0 +1,18 @@
+<script lang="ts">
+	export let length;
+	export let onInput: (value: string) => void;
+	let content: string = '';
+</script>
+
+<span class="relative text-blue-500 font-mono tracking-widest px-1"
+	><!--
+	--><input
+		class="absolute bg-transparent text-transparent w-full caret-blue-500 focus:outline-none focus:ring-0"
+		bind:value={content}
+		on:input={(event) => onInput(event.target.value)}
+		maxlength={length}
+	/><!-- 
+	-->{#each Array.from({ length }) as _, i}
+		<label>{content[i] || '_'}</label>
+	{/each}
+</span>
diff --git a/frontend/src/routes/tests/[id]/+page.svelte b/frontend/src/routes/tests/[id]/+page.svelte
index 151014c4a446224b6603efc75307c066160490bb..e21b403f5b886f885cd2dacd633fd6e1b89ce214 100644
--- a/frontend/src/routes/tests/[id]/+page.svelte
+++ b/frontend/src/routes/tests/[id]/+page.svelte
@@ -6,6 +6,8 @@
 	import { toastWarning } from '$lib/utils/toasts.js';
 	import { get } from 'svelte/store';
 	import User from '$lib/types/user.js';
+	import type SurveyGroup from '$lib/types/surveyGroup';
+	import Gapfill from '$lib/components/surveys/gapfill.svelte';
 
 	export let data;
 
@@ -16,14 +18,36 @@
 		Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
 	let startTime = new Date().getTime();
 
-	$: step = user ? 1 : 0;
+	function getSortedQuestions(group: SurveyGroup) {
+		return group.questions.sort(() => Math.random() - 0.5);
+	}
+
+	$: step = user ? 2 : 0;
 	$: uuid = user?.email || '';
 
-	$: currentGroupId = 0;
-	$: currentGroup = survey.groups[currentGroupId];
-	$: questionsRandomized = currentGroup.questions.sort(() => Math.random() - 0.5);
-	$: currentQuestionId = 0;
-	$: currentQuestion = questionsRandomized[currentQuestionId];
+	let currentGroupId = 0;
+	let currentGroup = survey.groups[currentGroupId];
+	let questionsRandomized = getSortedQuestions(currentGroup);
+	let currentQuestionId = 0;
+	let currentQuestion = questionsRandomized[currentQuestionId];
+	let type = currentQuestion.question.split(':')[0];
+	let value = currentQuestion.question.split(':').slice(1).join(':');
+	let gaps = type === 'gap' ? gapParts(currentQuestion.question) : null;
+
+	function setGroupId(id: number) {
+		currentGroupId = id;
+		currentGroup = survey.groups[currentGroupId];
+		questionsRandomized = getSortedQuestions(currentGroup);
+		setQuestionId(0);
+	}
+
+	function setQuestionId(id: number) {
+		currentQuestionId = id;
+		currentQuestion = questionsRandomized[currentQuestionId];
+		type = currentQuestion.question.split(':')[0];
+		value = currentQuestion.question.split(':').slice(1).join(':');
+		gaps = type === 'gap' ? gapParts(currentQuestion.question) : null;
+	}
 
 	async function selectOption(option: string) {
 		if (
@@ -40,7 +64,37 @@
 			return;
 		}
 		if (currentQuestionId < questionsRandomized.length - 1) {
-			currentQuestionId++;
+			setQuestionId(currentQuestionId + 1);
+			startTime = new Date().getTime();
+		} else {
+			nextGroup();
+		}
+	}
+
+	async function sendGap() {
+		if (!gaps) return;
+
+		const gapTexts = gaps
+			.filter((part) => part.gap !== null)
+			.map((part) => part.gap)
+			.join('|');
+
+		if (
+			!(await sendSurveyResponseAPI(
+				uuid,
+				sid,
+				survey.id,
+				currentGroupId,
+				currentQuestionId,
+				-1,
+				(new Date().getTime() - startTime) / 1000,
+				gapTexts
+			))
+		) {
+			return;
+		}
+		if (currentQuestionId < questionsRandomized.length - 1) {
+			setQuestionId(currentQuestionId + 1);
 			startTime = new Date().getTime();
 		} else {
 			nextGroup();
@@ -49,8 +103,7 @@
 
 	function nextGroup() {
 		if (currentGroupId < survey.groups.length - 1) {
-			currentGroupId++;
-			currentQuestionId = 0;
+			setGroupId(currentGroupId + 1);
 		} else {
 			step++;
 		}
@@ -68,6 +121,22 @@
 
 		step = 1;
 	}
+
+	function gapParts(question: string): { text: string; gap: string | null }[] {
+		if (!question.startsWith('gap:')) return [];
+
+		const gapText = question.split(':').slice(1).join(':');
+
+		const parts: { text: string; gap: string | null }[] = [];
+
+		for (let part of gapText.split(/(<.+?>)/g)) {
+			const isGap = part.startsWith('<') && part.endsWith('>');
+			const text = isGap ? part.slice(1, -1) : part;
+			parts.push({ text: text, gap: isGap ? '' : null });
+		}
+
+		return parts;
+	}
 </script>
 
 {#if step == 0}
@@ -76,7 +145,7 @@
 		<div class="flex mt-8">
 			<div class="grow border-r-gray-300 border-r py-16">
 				<p class="mb-4">{$t('surveys.loginUser')}</p>
-				<a href="/login?redirect=/surveys/{survey.id}" class="button">{$t('button.login')}</a>
+				<a href="/login?redirect=/tests/{survey.id}" class="button">{$t('button.login')}</a>
 			</div>
 			<div class="grow py-16">
 				<p class="mb-4">{$t('surveys.loginEmail')}</p>
@@ -97,58 +166,70 @@
 		<button class="button" on:click={() => step++}>{$t('button.next')}</button>
 	</div>
 {:else if step == 2}
-	{@const type = currentQuestion.question.split(':')[0]}
-	{@const value = currentQuestion.question.split(':').slice(1).join(':')}
-	<div class="mx-auto mt-16 text-center">
-		{#if type == 'text'}
-			<pre>{value}</pre>
-		{:else if type == 'image'}
-			<img src={value} alt="Question" />
-		{:else if type == 'audio'}
-			<audio controls autoplay class="rounded-lg mx-auto">
-				<source src={value} type="audio/mpeg" />
-				Your browser does not support the audio element.
-			</audio>
-		{/if}
-	</div>
-
-	<div class="mx-auto mt-16">
-		<div class="flex justify-around min-w-[600px] space-x-10">
-			{#each currentQuestion?.options as option (option)}
-				{@const type = option.split(':')[0]}
-				{@const value = option.split(':').slice(1).join(':')}
-				<div
-					class="h-48 w-48 overflow-hidden rounded-lg border border-black"
-					on:click={() => selectOption(option)}
-					role="button"
-					on:keydown={() => selectOption(option)}
-					tabindex="0"
-				>
-					{#if type === 'text'}
-						<span
-							class="flex items-center justify-center h-full w-full text-2xl transition-transform duration-200 ease-in-out transform hover:scale-105"
-						>
-							{value}
-						</span>
-					{:else if type === 'image'}
-						<img
-							src={value}
-							alt="Option {option}"
-							class="object-cover h-full w-full transition-transform duration-200 ease-in-out transform hover:scale-105"
-						/>
-					{:else if type == 'audio'}
-						<audio controls class="w-full" on:click|preventDefault|stopPropagation>
-							<source src={value} type="audio/mpeg" />
-							Your browser does not support the audio element.
-						</audio>
+	{#if type == 'gap'}
+		<div class="mx-auto mt-16 center flex flex-col">
+			<div>
+				{#each gaps as part}
+					{#if part.gap !== null}
+						<Gapfill length={part.text.length} onInput={(text) => (part.gap = text)} />
+					{:else}
+						{part.text}
 					{/if}
-				</div>
-			{/each}
+				{/each}
+			</div>
+			<button class="button mt-8" on:click={sendGap}>{$t('button.next')}</button>
 		</div>
-	</div>
+	{:else}
+		<div class="mx-auto mt-16 text-center">
+			{#if type == 'text'}
+				<pre>{value}</pre>
+			{:else if type == 'image'}
+				<img src={value} alt="Question" />
+			{:else if type == 'audio'}
+				<audio controls autoplay class="rounded-lg mx-auto">
+					<source src={value} type="audio/mpeg" />
+					Your browser does not support the audio element.
+				</audio>
+			{/if}
+		</div>
+
+		<div class="mx-auto mt-16">
+			<div class="flex justify-around min-w-[600px] space-x-10">
+				{#each currentQuestion?.options as option (option)}
+					{@const type = option.split(':')[0]}
+					{@const value = option.split(':').slice(1).join(':')}
+					<div
+						class="h-48 w-48 overflow-hidden rounded-lg border border-black"
+						on:click={() => selectOption(option)}
+						role="button"
+						on:keydown={() => selectOption(option)}
+						tabindex="0"
+					>
+						{#if type === 'text'}
+							<span
+								class="flex items-center justify-center h-full w-full text-2xl transition-transform duration-200 ease-in-out transform hover:scale-105"
+							>
+								{value}
+							</span>
+						{:else if type === 'image'}
+							<img
+								src={value}
+								alt="Option {option}"
+								class="object-cover h-full w-full transition-transform duration-200 ease-in-out transform hover:scale-105"
+							/>
+						{:else if type == 'audio'}
+							<audio controls class="w-full" on:click|preventDefault|stopPropagation>
+								<source src={value} type="audio/mpeg" />
+								Your browser does not support the audio element.
+							</audio>
+						{/if}
+					</div>
+				{/each}
+			</div>
+		</div>
+	{/if}
 {:else if step == 3}
 	<div class="mx-auto mt-16 text-center">
-		flex maximize width
 		<h1>{$t('surveys.complete')}</h1>
 	</div>
 {/if}
diff --git a/scripts/surveys/groups.csv b/scripts/surveys/groups.csv
index 8d6c9a8b8ad918724b2f8f2caf280cbe47c980da..77dd4cc36443afa0424919dd3ae7405dcbf75197 100644
--- a/scripts/surveys/groups.csv
+++ b/scripts/surveys/groups.csv
@@ -1,3 +1,4 @@
 id,title,options
 1,Auditory Picture Vocabulary Test - English,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179
 2,Test2,2
+3,Gap,3
diff --git a/scripts/surveys/surveys.csv b/scripts/surveys/surveys.csv
index 7d69ae18ac86a8be09f7c350d35bc516501acceb..6710c9c2ffd48de780ea30ceb6153d87061a166e 100644
--- a/scripts/surveys/surveys.csv
+++ b/scripts/surveys/surveys.csv
@@ -1,3 +1,4 @@
 id,title,groups
 1,Title,1,2
 2,Title,2
+3,TitleGap,3