diff --git a/backend/app/crud.py b/backend/app/crud.py
index cf1c8753dfac16c55edc31752e9fb394befca67d..1e92bcb259a832ac7109153189d16cb091394998 100644
--- a/backend/app/crud.py
+++ b/backend/app/crud.py
@@ -144,6 +144,10 @@ def create_session_satisfy(
     return db_satisfy
 
 
+def get_message(db: Session, message_id: int):
+    return db.query(models.Message).filter(models.Message.id == message_id).first()
+
+
 def get_messages(db: Session, session_id: int, skip: int = 0, limit: int = 100):
     return (
         db.query(models.Message)
@@ -188,6 +192,26 @@ def create_message_metadata(
     return db_message_metadata
 
 
+def create_message_spellcheck(
+    db: Session,
+    message_id: int,
+    message: str,
+    spellcheck: schemas.MessageSpellCheckCreate,
+):
+    message = (
+        message[: spellcheck.start]
+        + "¤µ"
+        + message[spellcheck.start : spellcheck.end]
+        + "µ¤"
+        + message[spellcheck.end :]
+    )
+
+    db.query(models.Message).filter(models.Message.id == message_id).update(
+        {"content": message}
+    )
+    db.commit()
+
+
 def create_test_typing(db: Session, test: schemas.TestTypingCreate, user: schemas.User):
     db_test = models.TestTyping(user_id=user.id)
     db.add(db_test)
diff --git a/backend/app/main.py b/backend/app/main.py
index 1260eea590edede2285f53b9779eee854d214782..9c7f18d0b92d74877529dc1b5898746133b54417 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -511,13 +511,14 @@ def read_sessions(
 
     return crud.get_sessions(db, current_user, skip=skip, limit=limit)
 
-@sessionsRouter.post('/{session_id}/satisfy', status_code=status.HTTP_204_NO_CONTENT)
+
+@sessionsRouter.post("/{session_id}/satisfy", status_code=status.HTTP_204_NO_CONTENT)
 def create_session_satisfy(
-        session_id: int,
-        satisfy: schemas.SessionSatisfyCreate,
-        db: Session = Depends(get_db),
-        current_user: schemas.User = Depends(get_jwt_user),
-        ):
+    session_id: int,
+    satisfy: schemas.SessionSatisfyCreate,
+    db: Session = Depends(get_db),
+    current_user: schemas.User = Depends(get_jwt_user),
+):
     db_session = crud.get_session(db, session_id)
     if db_session is None:
         raise HTTPException(status_code=404, detail="Session not found")
@@ -611,6 +612,37 @@ def create_message(
     return {"id": message.id, "message_id": message.message_id}
 
 
+@sessionsRouter.post(
+    "/{session_id}/messages/{message_id}/spellcheck",
+    status_code=status.HTTP_204_NO_CONTENT,
+)
+def spellcheck_message(
+    session_id: int,
+    message_id: int,
+    spellcheck: schemas.MessageSpellCheckCreate,
+    db: Session = Depends(get_db),
+    current_user: schemas.User = Depends(get_jwt_user),
+):
+    db_session = crud.get_session(db, session_id)
+    if db_session is None:
+        raise HTTPException(status_code=404, detail="Session not found")
+
+    if (
+        not check_user_level(current_user, models.UserType.ADMIN)
+        and current_user not in db_session.users
+    ):
+        raise HTTPException(
+            status_code=401,
+            detail="You do not have permission to spellcheck a message in this session",
+        )
+
+    message = crud.get_message(db, message_id)
+    if message is None:
+        raise HTTPException(status_code=404, detail="Message not found")
+
+    crud.create_message_spellcheck(db, message_id, message.content, spellcheck)
+
+
 async def send_websoket_typing(session_id: int, user_id: int):
     content = json.dumps(
         {"type": "message", "action": "typing", "data": {"user": user_id}}
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 7697f91d76c3b7dc3766812e38d43375145a12bd..f5725ed215a6fe7b6719b738903e980d5cd18b6e 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -128,6 +128,11 @@ class MessageCreate(BaseModel):
         from_attributes = True
 
 
+class MessageSpellCheckCreate(BaseModel):
+    start: int
+    end: int
+
+
 class TestTypingEntryCreate(BaseModel):
     exerciceId: int
     position: int
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4dee433a2ce14baf9913c32d5343297d2519b18a..240f04474e8e326b7e881cf5ba246dd33fbb296d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
 			"version": "0.0.1",
 			"dependencies": {
 				"emoji-picker-element": "^1.21.3",
+				"sanitize-html": "^2.13.0",
 				"svelte-gravatar": "^1.0.3",
 				"svelte-i18n": "^4.0.0",
 				"svelte-material-icons": "^3.0.5",
@@ -1744,12 +1745,13 @@
 			}
 		},
 		"node_modules/braces": {
-			"version": "3.0.2",
-			"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-			"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+			"version": "3.0.3",
+			"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+			"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
-				"fill-range": "^7.0.1"
+				"fill-range": "^7.1.1"
 			},
 			"engines": {
 				"node": ">=8"
@@ -2130,7 +2132,6 @@
 			"version": "4.3.1",
 			"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
 			"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.10.0"
 			}
@@ -2203,6 +2204,61 @@
 				"node": ">=6.0.0"
 			}
 		},
+		"node_modules/dom-serializer": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+			"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+			"license": "MIT",
+			"dependencies": {
+				"domelementtype": "^2.3.0",
+				"domhandler": "^5.0.2",
+				"entities": "^4.2.0"
+			},
+			"funding": {
+				"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+			}
+		},
+		"node_modules/domelementtype": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+			"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+			"funding": [
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/fb55"
+				}
+			],
+			"license": "BSD-2-Clause"
+		},
+		"node_modules/domhandler": {
+			"version": "5.0.3",
+			"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+			"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"domelementtype": "^2.3.0"
+			},
+			"engines": {
+				"node": ">= 4"
+			},
+			"funding": {
+				"url": "https://github.com/fb55/domhandler?sponsor=1"
+			}
+		},
+		"node_modules/domutils": {
+			"version": "3.1.0",
+			"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+			"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+			"license": "BSD-2-Clause",
+			"dependencies": {
+				"dom-serializer": "^2.0.0",
+				"domelementtype": "^2.3.0",
+				"domhandler": "^5.0.3"
+			},
+			"funding": {
+				"url": "https://github.com/fb55/domutils?sponsor=1"
+			}
+		},
 		"node_modules/eastasianwidth": {
 			"version": "0.2.0",
 			"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2226,6 +2282,18 @@
 			"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
 			"dev": true
 		},
+		"node_modules/entities": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+			"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+			"license": "BSD-2-Clause",
+			"engines": {
+				"node": ">=0.12"
+			},
+			"funding": {
+				"url": "https://github.com/fb55/entities?sponsor=1"
+			}
+		},
 		"node_modules/errno": {
 			"version": "0.1.8",
 			"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
@@ -2348,7 +2416,6 @@
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 			"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-			"dev": true,
 			"engines": {
 				"node": ">=10"
 			},
@@ -2701,10 +2768,11 @@
 			}
 		},
 		"node_modules/fill-range": {
-			"version": "7.0.1",
-			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-			"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+			"version": "7.1.1",
+			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+			"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"to-regex-range": "^5.0.1"
 			},
@@ -2974,6 +3042,25 @@
 				"node": ">= 0.4"
 			}
 		},
+		"node_modules/htmlparser2": {
+			"version": "8.0.2",
+			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+			"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+			"funding": [
+				"https://github.com/fb55/htmlparser2?sponsor=1",
+				{
+					"type": "github",
+					"url": "https://github.com/sponsors/fb55"
+				}
+			],
+			"license": "MIT",
+			"dependencies": {
+				"domelementtype": "^2.3.0",
+				"domhandler": "^5.0.3",
+				"domutils": "^3.0.1",
+				"entities": "^4.4.0"
+			}
+		},
 		"node_modules/iconv-lite": {
 			"version": "0.6.3",
 			"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -3157,6 +3244,7 @@
 			"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 			"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=0.12.0"
 			}
@@ -3170,6 +3258,15 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/is-plain-object": {
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+			"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.10.0"
+			}
+		},
 		"node_modules/is-promise": {
 			"version": "2.2.2",
 			"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
@@ -3602,7 +3699,6 @@
 			"version": "3.3.7",
 			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
 			"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "github",
@@ -3764,6 +3860,12 @@
 				"node": ">= 0.10"
 			}
 		},
+		"node_modules/parse-srcset": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+			"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
+			"license": "MIT"
+		},
 		"node_modules/path-exists": {
 			"version": "4.0.0",
 			"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3844,8 +3946,7 @@
 		"node_modules/picocolors": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-			"dev": true
+			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
 		},
 		"node_modules/picomatch": {
 			"version": "2.3.1",
@@ -3882,7 +3983,6 @@
 			"version": "8.4.38",
 			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
 			"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "opencollective",
@@ -4308,6 +4408,20 @@
 				"rimraf": "bin.js"
 			}
 		},
+		"node_modules/sanitize-html": {
+			"version": "2.13.0",
+			"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz",
+			"integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==",
+			"license": "MIT",
+			"dependencies": {
+				"deepmerge": "^4.2.2",
+				"escape-string-regexp": "^4.0.0",
+				"htmlparser2": "^8.0.0",
+				"is-plain-object": "^5.0.0",
+				"parse-srcset": "^1.0.2",
+				"postcss": "^8.3.11"
+			}
+		},
 		"node_modules/sax": {
 			"version": "1.3.0",
 			"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
@@ -5421,6 +5535,7 @@
 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 			"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"is-number": "^7.0.0"
 			},
diff --git a/frontend/package.json b/frontend/package.json
index 352ca2baa2fc12a76287e6564349d05d9ae5f9d9..5548b0f003d7d324068776f5e0ee41fcd3ecce70 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -47,6 +47,7 @@
 	"type": "module",
 	"dependencies": {
 		"emoji-picker-element": "^1.21.3",
+		"sanitize-html": "^2.13.0",
 		"svelte-gravatar": "^1.0.3",
 		"svelte-i18n": "^4.0.0",
 		"svelte-material-icons": "^3.0.5",
diff --git a/frontend/src/lib/api/sessions.ts b/frontend/src/lib/api/sessions.ts
index f0d5898b8a58e14f73fb27f7c20a428fb4df278a..c62ad1ebf68da53698de8b5378243a148a80bf88 100644
--- a/frontend/src/lib/api/sessions.ts
+++ b/frontend/src/lib/api/sessions.ts
@@ -75,6 +75,23 @@ export async function updateMessageAPI(
 	return response.data;
 }
 
+export async function addMessageSpellCheckAPI(
+	id: number,
+	message_id: number,
+	start: number,
+	end: number
+): Promise<boolean> {
+	const response = await axiosInstance.post(`/sessions/${id}/messages/${message_id}/spellcheck`, {
+		start,
+		end
+	});
+	if (response.status !== 204) {
+		toastAlert('Failed to add spellcheck');
+		return false;
+	}
+	return true;
+}
+
 export async function patchLanguageAPI(id: number, language: string) {
 	const response = await axiosInstance.patch(`/sessions/${id}`, { language });
 
diff --git a/frontend/src/lib/components/icons/spellCheck.svelte b/frontend/src/lib/components/icons/spellCheck.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..c215e280141ffe9c934f8a22924606386987e2de
--- /dev/null
+++ b/frontend/src/lib/components/icons/spellCheck.svelte
@@ -0,0 +1,10 @@
+<svg
+	fill="currentColor"
+	width="20"
+	height="20"
+	viewBox="0 -32 576 576"
+	xmlns="http://www.w3.org/2000/svg"
+	><path
+		d="M272 256h91.36c43.2 0 82-32.2 84.51-75.34a79.82 79.82 0 0 0-25.26-63.07 79.81 79.81 0 0 0 9.06-44.91C427.9 30.57 389.3 0 347 0h-75a16 16 0 0 0-16 16v224a16 16 0 0 0 16 16zm40-200h40a24 24 0 0 1 0 48h-40zm0 96h56a24 24 0 0 1 0 48h-56zM155.12 22.25A32 32 0 0 0 124.64 0H99.36a32 32 0 0 0-30.48 22.25L.59 235.73A16 16 0 0 0 16 256h24.93a16 16 0 0 0 15.42-11.73L68.29 208h87.42l11.94 36.27A16 16 0 0 0 183.07 256H208a16 16 0 0 0 15.42-20.27zM89.37 144L112 75.3l22.63 68.7zm482 132.48l-45.21-45.3a15.88 15.88 0 0 0-22.59 0l-151.5 151.5-55.41-55.5a15.88 15.88 0 0 0-22.59 0l-45.3 45.3a16 16 0 0 0 0 22.59l112 112.21a15.89 15.89 0 0 0 22.6 0l208-208.21a16 16 0 0 0-.02-22.59z"
+	/></svg
+>
diff --git a/frontend/src/lib/components/sessions/message.svelte b/frontend/src/lib/components/sessions/message.svelte
index e504be6081b2c9f2c0e18d6850010dee4c942cf8..f056d1450df621b0d81482b44a0f0283313fcd76 100644
--- a/frontend/src/lib/components/sessions/message.svelte
+++ b/frontend/src/lib/components/sessions/message.svelte
@@ -1,10 +1,20 @@
 <script lang="ts">
 	import type Message from '$lib/types/message';
 	import { displayTime } from '$lib/utils/date';
-	import { Check, Icon, Pencil } from 'svelte-hero-icons';
+	import {
+		ChatBubbleBottomCenter,
+		ChatBubbleBottomCenterText,
+		Check,
+		Icon,
+		Pencil,
+		PencilSquare
+	} from 'svelte-hero-icons';
 	import { user } from '$lib/types/user';
 	import Gravatar from 'svelte-gravatar';
 	import { t } from '$lib/services/i18n';
+	import { onMount } from 'svelte';
+	import SpellCheck from '$lib/components/icons/spellCheck.svelte';
+	import { sanitize } from '$lib/utils/sanitize';
 
 	export let message: Message;
 
@@ -48,6 +58,85 @@
 		}
 	}
 
+	let hightlight: HTMLDivElement;
+
+	onMount(() => {
+		document.addEventListener('selectionchange', onTextSelect);
+	});
+
+	function getSelectionCharacterOffsetWithin() {
+		var start = 0;
+		var end = 0;
+		var doc = contentDiv.ownerDocument;
+		var win = doc.defaultView;
+		if (!doc || !win) return { start: 0, end: 0 };
+		var sel;
+		if (typeof win.getSelection === 'undefined') {
+			return { start: 0, end: 0 };
+		}
+		sel = win.getSelection();
+		if (!sel) return { start: 0, end: 0 };
+		if (sel.rangeCount <= 0) return { start: 0, end: 0 };
+
+		var range = sel.getRangeAt(0);
+		var preCaretRange = range.cloneRange();
+		preCaretRange.selectNodeContents(contentDiv);
+		preCaretRange.setEnd(range.startContainer, range.startOffset);
+		start = preCaretRange.toString().length;
+		preCaretRange.setEnd(range.endContainer, range.endOffset);
+		end = preCaretRange.toString().length;
+
+		return { start: start, end: end };
+	}
+
+	function onTextSelect() {
+		const selection = window.getSelection();
+		if (!selection) return;
+		const range = selection.getRangeAt(0);
+		const start = range.startOffset;
+		const end = range.endOffset;
+		if (range.commonAncestorContainer.parentElement === contentDiv && end - start > 0) {
+			const rects = range.getClientRects();
+			if (!rects.length) {
+				hightlight.style.visibility = 'hidden';
+				return;
+			}
+			const rect = rects[rects.length - 1];
+			if (!rect) {
+				hightlight.style.visibility = 'hidden';
+				return;
+			}
+			hightlight.style.top = rect.bottom + 'px';
+			hightlight.style.left = rect.right + 'px';
+			hightlight.style.visibility = 'visible';
+		} else {
+			hightlight.style.visibility = 'hidden';
+		}
+	}
+
+	async function onSpellSelect() {
+		const selection = window.getSelection();
+		if (!selection) {
+			hightlight.style.visibility = 'hidden';
+			return;
+		}
+		const range = getSelectionCharacterOffsetWithin();
+
+		const start = range.start;
+		const end = range.end;
+		console.log(start, end);
+
+		const res = await message.addSpellCheck(start, end);
+
+		if (res) {
+			selection.removeAllRanges();
+			hightlight.style.visibility = 'hidden';
+			contentDiv.innerHTML = sanitize(message.content)
+				.replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">')
+				.replaceAll('µ¤', '</span>');
+		}
+	}
+
 	const isSender = message.user.id == $user?.id;
 </script>
 
@@ -68,7 +157,9 @@
 		class:text-white={isSender}
 	>
 		<div contenteditable={isEdit} bind:this={contentDiv} class:bg-blue-900={isEdit}>
-			{message.content}
+			{@html sanitize(message.content)
+				.replaceAll('¤µ', '<span class="decoration-wavy decoration-orange-500 underline">')
+				.replaceAll('µ¤', '</span>')}
 		</div>
 		{#if isEdit}
 			<button
@@ -106,7 +197,14 @@
 					<div>
 						{#each $messageVersions as version}
 							<div class="flex justify-between items-center border-b border-gray-300 py-1">
-								<div>{version.content}</div>
+								<div>
+									{@html sanitize(version.content)
+										.replaceAll(
+											'¤µ',
+											'<span class="decoration-wavy decoration-orange-500 underline">'
+										)
+										.replaceAll('µ¤', '</span>')}
+								</div>
 								<div class="whitespace-nowrap">{displayTime(version.date)}</div>
 							</div>
 						{/each}
@@ -121,3 +219,17 @@
 		{/if}
 	</div>
 </div>
+
+<div class="absolute invisible rounded-full border-black border bg-white" bind:this={hightlight}>
+	<button
+		on:click={onSpellSelect}
+		class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pl-4 rounded-l-full"
+	>
+		<SpellCheck />
+	</button><!---
+	--><button
+		class="bg-opacity-0 bg-blue-500 hover:bg-opacity-50 p-2 pr-4 rounded-r-full hover:cursor-not-allowed"
+	>
+		<Icon src={ChatBubbleBottomCenterText} size="20" />
+	</button>
+</div>
diff --git a/frontend/src/lib/types/message.ts b/frontend/src/lib/types/message.ts
index fefb5d925631246bb92e75aca4970e08005643e7..87acfd307db6cfe4807a45d9428ca93911d85f52 100644
--- a/frontend/src/lib/types/message.ts
+++ b/frontend/src/lib/types/message.ts
@@ -1,6 +1,6 @@
 import Session from './session';
 import User from './user';
-import { updateMessageAPI } from '$lib/api/sessions';
+import { updateMessageAPI, addMessageSpellCheckAPI } from '$lib/api/sessions';
 import { toastAlert } from '$lib/utils/toasts';
 import { writable, type Writable } from 'svelte/store';
 
@@ -81,6 +81,28 @@ export default class Message {
 		return true;
 	}
 
+	async addSpellCheck(start: number, end: number): Promise<boolean> {
+		for (let i = 0; i < start + 1; i++) {
+			if (this._content[i] == '¤' || this._content[i] == 'µ') {
+				start++;
+				end++;
+			}
+		}
+
+		const response = await addMessageSpellCheckAPI(this._session.id, this._id, start, end);
+
+		if (!response) return false;
+
+		this._content =
+			this._content.slice(0, start) +
+			'¤µ' +
+			this._content.slice(start, end) +
+			'µ¤' +
+			this._content.slice(end);
+
+		return true;
+	}
+
 	static parse(
 		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 		json: any,
@@ -148,6 +170,7 @@ export default class Message {
 			if (prev.created_at < m.created_at) {
 				prev._versions.update((v) => [...v, { content: m.content, date: m.created_at }]);
 				prev._content = m.content;
+				prev._id = m.id;
 				prev._edited = true;
 			}
 		}
diff --git a/frontend/src/lib/utils/sanitize.ts b/frontend/src/lib/utils/sanitize.ts
new file mode 100644
index 0000000000000000000000000000000000000000..38111abc688bb323dff5d0898963584a30f5b6ea
--- /dev/null
+++ b/frontend/src/lib/utils/sanitize.ts
@@ -0,0 +1,8 @@
+import sanitizeHtml from 'sanitize-html';
+
+export function sanitize(html: string): string {
+	return sanitizeHtml(html, {
+		allowedTags: [],
+		allowedAttributes: {}
+	});
+}