diff --git a/ivette/package.json b/ivette/package.json
index 369ffc110013197a1791b84eaff850e10dbfd960..6978f96fcb82f78ec913022f3c5a775ba531916f 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -52,6 +52,7 @@
     "react-force-graph-2d": "^1.25.4",
     "react-force-graph-3d": "^1.24.2",
     "react-infinite-scroller": "^1.2.6",
+    "react-markdown": "9.0.1",
     "react-pivottable": "^0.11.0",
     "react-virtualized": "9.22.5",
     "react-virtualized-auto-sizer": "^1.0.22",
diff --git a/ivette/src/dome/renderer/text/markdown.tsx b/ivette/src/dome/renderer/text/markdown.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..930f2a63fce2816905751d261526cdf0aef70707
--- /dev/null
+++ b/ivette/src/dome/renderer/text/markdown.tsx
@@ -0,0 +1,151 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2024                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It 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 Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import React from 'react';
+import ReactMarkdown, { Components, Options } from 'react-markdown';
+
+import { classes } from 'dome/misc/utils';
+import { Icon } from 'dome/controls/icons';
+
+export interface IReplacement {
+  regex: RegExp,
+  transform: (key: number, match?: RegExpExecArray) => JSX.Element | null
+}
+
+export const iconTagReplacement: IReplacement = {
+  regex: /\[icon-([^\]]+)\]/g,
+  transform: (key: number, match?: RegExpExecArray) => {
+    return match ? <Icon key={key} id={match[1]}/> : null;
+  }
+};
+
+// --------------------------------------------------------------------------
+// --- Replacement function
+// --------------------------------------------------------------------------
+/**
+ * Replace all tag in the text.
+ * This function doesn't replace any tags added by a previous replacement.
+ */
+function replaceTagsByElement(
+  text: string,
+  replacement?: IReplacement[]
+): (string | JSX.Element | null)[] {
+  if(!replacement || replacement.length < 1) return [text];
+
+  const { regex, transform } = replacement[0];
+  replacement.shift();
+
+  const newContent = [];
+  let match;
+  let lastIndex = 0;
+  while ((match = regex.exec(text)) !== null) {
+    if (match.index > lastIndex) {
+      const before = replaceTagsByElement(
+        text.slice(lastIndex, match.index), replacement
+      );
+      before.forEach((elt) => newContent.push(elt));
+    }
+    newContent.push(transform(Math.random(), match));
+    lastIndex = regex.lastIndex;
+  }
+  if (lastIndex < text.length) {
+    const after = replaceTagsByElement(text.slice(lastIndex), replacement);
+    after.forEach((elt) => newContent.push(elt));
+  }
+  return newContent;
+}
+
+function replaceTags(
+  children: React.ReactNode,
+  replacement: IReplacement[]
+): React.ReactNode {
+  const childrenTab = React.Children.toArray(children);
+
+  const newContent = childrenTab.map((child) => {
+    if (typeof child === 'string') {
+      return replaceTagsByElement(child, replacement.slice());
+    }
+    return child;
+  });
+
+  return newContent;
+}
+
+// --------------------------------------------------------------------------
+// --- Markdown component
+// --------------------------------------------------------------------------
+type tagHtmlList = [ k: keyof Components, v: keyof Components ][]
+
+interface MarkdownProps {
+  /** classes for Markdown component */
+  className?: string;
+  /** html tag of the markdown to be processed and possible replacement */
+  htmlTag?: tagHtmlList;
+  /** Tab of tag replacement */
+  replacement?: IReplacement[];
+  /** Children */
+  children?: string | null;
+}
+
+export function Markdown(
+  props: MarkdownProps
+): JSX.Element {
+  const { className, replacement, htmlTag, children } = props;
+  const markdownClasses = classes(
+    "dome-xMarkdown", "dome-pages",
+    className,
+  );
+
+  const transformChildren = (c: React.ReactNode): React.ReactNode => {
+    return !replacement ? c : replaceTags(c, replacement);
+  };
+
+  const getComponentsOption = (): Components | null => {
+    if(!htmlTag) return null;
+
+    const getDynamicElement = (
+      tagName: keyof JSX.IntrinsicElements,
+      children: React.ReactNode
+    ): JSX.Element => {
+      const Tag = tagName;
+      return <Tag>{children}</Tag>;
+    };
+
+    const component = [];
+    for (const [key, val] of htmlTag) {
+      component.push([key, ({ children }: {
+        children: React.ReactNode;
+      }) => getDynamicElement(val, transformChildren(children))]);
+    }
+
+    return Object.fromEntries(component);
+  };
+
+  const options: Options = {
+    className: markdownClasses,
+    components: getComponentsOption()
+  };
+
+  return <ReactMarkdown {...options}>{ children }</ReactMarkdown>;
+}
+
+/* -------------------------------------------------------------------------- */
diff --git a/ivette/src/dome/renderer/text/style.css b/ivette/src/dome/renderer/text/style.css
index ded51521f5765f05759fc25a8ca9bd0721db3ace..b3a5e8e8d93b23a2e4198f17672f1e9e6231b990 100644
--- a/ivette/src/dome/renderer/text/style.css
+++ b/ivette/src/dome/renderer/text/style.css
@@ -213,3 +213,11 @@
 .cm-line.cm-activeLine, .cm-active-line { background-color: var(--background); }
 
 /* -------------------------------------------------------------------------- */
+/* --- Markdown                                                           --- */
+/* -------------------------------------------------------------------------- */
+
+.dome-xMarkdown {
+  &>* { padding: 1px; }
+}
+
+/* -------------------------------------------------------------------------- */
diff --git a/ivette/src/dome/template/makefile.packages b/ivette/src/dome/template/makefile.packages
index ba3fa7df3f6160b836499cb1329696c168061c2e..5e9e6750694ecd4cc4473995d879065c8fe183b7 100644
--- a/ivette/src/dome/template/makefile.packages
+++ b/ivette/src/dome/template/makefile.packages
@@ -58,6 +58,7 @@ DOME_APP_PACKAGES= \
 	react-force-graph-2d@^1.25.4 \
 	react-force-graph-3d@^1.24.2 \
 	d3-selection@^3 \
-	d3-graphviz@^5
+	d3-graphviz@^5 \
+	react-markdown@9.0.1 \
 
 # --------------------------------------------------------------------------