diff --git a/backend/src/baserow/contrib/builder/elements/element_types.py b/backend/src/baserow/contrib/builder/elements/element_types.py
index 505afadca..1064141ef 100644
--- a/backend/src/baserow/contrib/builder/elements/element_types.py
+++ b/backend/src/baserow/contrib/builder/elements/element_types.py
@@ -451,12 +451,13 @@ class TextElementType(ElementType):
 
     type = "text"
     model_class = TextElement
-    serializer_field_names = ["value", "alignment"]
-    allowed_fields = ["value", "alignment"]
+    serializer_field_names = ["value", "alignment", "format"]
+    allowed_fields = ["value", "alignment", "format"]
 
     class SerializedDict(ElementDict):
         value: BaserowFormula
         alignment: str
+        format: str
 
     def get_pytest_params(self, pytest_data_fixture):
         return {
@@ -466,6 +467,7 @@ class TextElementType(ElementType):
             "Asperiores corporis perspiciatis nam harum veritatis. "
             "Impedit qui maxime aut illo quod ea molestias.'",
             "alignment": "left",
+            "format": TextElement.TEXT_FORMATS.PLAIN,
         }
 
     @property
@@ -479,6 +481,11 @@ class TextElementType(ElementType):
                 allow_blank=True,
                 default="",
             ),
+            "format": serializers.ChoiceField(
+                choices=TextElement.TEXT_FORMATS.choices,
+                default=TextElement.TEXT_FORMATS.PLAIN,
+                help_text=TextElement._meta.get_field("format").help_text,
+            ),
         }
 
     def import_serialized(self, page, serialized_values, id_mapping):
diff --git a/backend/src/baserow/contrib/builder/elements/models.py b/backend/src/baserow/contrib/builder/elements/models.py
index 0c5a4d1a5..c32456e9a 100644
--- a/backend/src/baserow/contrib/builder/elements/models.py
+++ b/backend/src/baserow/contrib/builder/elements/models.py
@@ -355,12 +355,22 @@ class TextElement(Element):
     A simple blob of text.
     """
 
+    class TEXT_FORMATS(models.TextChoices):
+        PLAIN = "plain"
+        MARKDOWN = "markdown"
+
     value = FormulaField(default="")
     alignment = models.CharField(
         choices=HorizontalAlignments.choices,
         max_length=10,
         default=HorizontalAlignments.LEFT,
     )
+    format = models.CharField(
+        choices=TEXT_FORMATS.choices,
+        help_text="The format of the text",
+        max_length=10,
+        default=TEXT_FORMATS.PLAIN,
+    )
 
 
 class LinkElement(Element):
diff --git a/backend/src/baserow/contrib/builder/migrations/0005_textelement_format.py b/backend/src/baserow/contrib/builder/migrations/0005_textelement_format.py
new file mode 100644
index 000000000..5c1a615ce
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/migrations/0005_textelement_format.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.0.10 on 2024-01-17 14:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("builder", "0004_image_element_sizing_styles"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="textelement",
+            name="format",
+            field=models.CharField(
+                choices=[("plain", "Plain"), ("markdown", "Markdown")],
+                default="plain",
+                help_text="The format of the text",
+                max_length=10,
+            ),
+        ),
+    ]
diff --git a/backend/tests/baserow/contrib/builder/test_builder_application_type.py b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
index 5ddaa4757..7fdf451f9 100644
--- a/backend/tests/baserow/contrib/builder/test_builder_application_type.py
+++ b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
@@ -228,6 +228,7 @@ def test_builder_application_export(data_fixture):
                         "style_background": "none",
                         "value": element2.value,
                         "alignment": "left",
+                        "format": TextElement.TEXT_FORMATS.PLAIN,
                     },
                     {
                         "id": element_container.id,
@@ -277,6 +278,7 @@ def test_builder_application_export(data_fixture):
                         "order": str(element_inside_container.order),
                         "value": element_inside_container.value,
                         "alignment": "left",
+                        "format": TextElement.TEXT_FORMATS.PLAIN,
                     },
                 ],
             },
diff --git a/web-frontend/modules/builder/components/elements/components/HeadingElement.vue b/web-frontend/modules/builder/components/elements/components/HeadingElement.vue
index 20c1c5d0b..3c65ac617 100644
--- a/web-frontend/modules/builder/components/elements/components/HeadingElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/HeadingElement.vue
@@ -8,11 +8,9 @@
     <component
       :is="`h${element.level}`"
       class="heading-element__heading"
+      :class="{ [`ab-heading--h${element.level}`]: true }"
       :style="{
         '--color': resolveColor(element.font_color, headingColorVariables),
-        '--font-size': `${
-          builder.theme[`heading_${element.level}_font_size`]
-        }px`,
       }"
     >
       {{ resolvedValue || $t('headingElement.noValue') }}
diff --git a/web-frontend/modules/builder/components/elements/components/TextElement.vue b/web-frontend/modules/builder/components/elements/components/TextElement.vue
index 1c085c3b9..0ef0a9052 100644
--- a/web-frontend/modules/builder/components/elements/components/TextElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/TextElement.vue
@@ -6,12 +6,28 @@
       [`element--alignment-horizontal-${element.alignment}`]: true,
     }"
   >
-    <p v-for="paragraph in paragraphs" :key="paragraph.id" class="ab-paragraph">
-      {{ paragraph.content }}
-    </p>
-    <p v-if="!paragraphs.length" class="ab-paragraph">
-      {{ $t('textElement.noValue') }}
-    </p>
+    <template v-if="element.format === TEXT_FORMAT_TYPES.MARKDOWN">
+      <MarkdownIt
+        v-if="resolvedValue"
+        class="ab-paragraph"
+        :content="resolvedValue"
+        :rules="rules"
+        @click.native="onClick"
+      ></MarkdownIt>
+      <p v-else class="ab-paragraph">{{ $t('textElement.noValue') }}</p>
+    </template>
+    <template v-else>
+      <p
+        v-for="paragraph in paragraphs"
+        :key="paragraph.id"
+        class="ab-paragraph"
+      >
+        {{ paragraph.content }}
+      </p>
+      <p v-if="!paragraphs.length" class="ab-paragraph">
+        {{ $t('textElement.noValue') }}
+      </p>
+    </template>
   </div>
 </template>
 
@@ -19,6 +35,8 @@
 import element from '@baserow/modules/builder/mixins/element'
 import { generateHash } from '@baserow/modules/core/utils/hashing'
 import { ensureString } from '@baserow/modules/core/utils/validator'
+import { TEXT_FORMAT_TYPES } from '@baserow/modules/builder/enums'
+import { prefixInternalResolvedUrl } from '@baserow/modules/builder/utils/urlResolution'
 
 /**
  * @typedef Text
@@ -34,6 +52,7 @@ export default {
      * @type {Object}
      * @property {Array.<Text>} value - A list of paragraphs
      * @property {string} alignment - The alignment of the element on the page
+     * @property {string} format - The format of the text
      */
     element: {
       type: Object,
@@ -58,6 +77,77 @@ export default {
           id: generateHash(line + index),
         }))
     },
+    TEXT_FORMAT_TYPES() {
+      return TEXT_FORMAT_TYPES
+    },
+    // Custom rules to pass down to `MarkdownIt` element.
+    // The goal is to make the styling of the rendered markdown content
+    // consistent with the rest of the application builder CSS classes
+    rules() {
+      return {
+        heading_open: (tokens, idx, options, env, renderer) => {
+          const level = tokens[idx].markup.length
+          tokens[idx].attrJoin('class', `ab-heading--h${level}`)
+          return renderer.renderToken(tokens, idx, options)
+        },
+        link_open: (tokens, idx, options, env, renderer) => {
+          const url = prefixInternalResolvedUrl(
+            tokens[idx].attrGet('href'),
+            this.builder,
+            'custom',
+            this.mode
+          )
+          tokens[idx].attrSet('href', url)
+          tokens[idx].attrJoin('class', 'link-element__link')
+          return renderer.renderToken(tokens, idx, options)
+        },
+        image: (tokens, idx, options, env, renderer) => {
+          tokens[idx].attrJoin('class', 'image_element__img')
+          return renderer.renderToken(tokens, idx, options)
+        },
+        paragraph_open: (tokens, idx, options, env, renderer) => {
+          tokens[idx].attrJoin('class', 'ab-paragraph')
+          return renderer.renderToken(tokens, idx, options)
+        },
+        table_open: (tokens, idx, options, env, renderer) => {
+          tokens[idx].attrJoin('class', 'baserow-table')
+          return renderer.renderToken(tokens, idx, options)
+        },
+        tr_open: (tokens, idx, options, env, renderer) => {
+          // Only apply this styling to the first row present in table header.
+          if (idx > 0 && tokens[idx - 1].type === 'thead_open') {
+            tokens[idx].attrJoin('class', 'baserow-table__header-row')
+          } else {
+            tokens[idx].attrJoin('class', 'baserow-table__row')
+          }
+          return renderer.renderToken(tokens, idx, options)
+        },
+        th_open: (tokens, idx, options, env, renderer) => {
+          tokens[idx].attrJoin('class', 'baserow-table__header-cell')
+          return renderer.renderToken(tokens, idx, options)
+        },
+        td_open: (tokens, idx, options, env, renderer) => {
+          tokens[idx].attrJoin('class', 'baserow-table__cell')
+          return renderer.renderToken(tokens, idx, options)
+        },
+      }
+    },
+  },
+  methods: {
+    onClick(event) {
+      if (this.mode === 'editing') {
+        event.preventDefault()
+        return
+      }
+      if (event.target.classList.contains('link-element__link')) {
+        const url = event.target.getAttribute('href')
+
+        if (url.startsWith('/')) {
+          event.preventDefault()
+          this.$router.push(url)
+        }
+      }
+    },
   },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/elements/components/forms/general/TextElementForm.vue b/web-frontend/modules/builder/components/elements/components/forms/general/TextElementForm.vue
index f714718ac..05eb34744 100644
--- a/web-frontend/modules/builder/components/elements/components/forms/general/TextElementForm.vue
+++ b/web-frontend/modules/builder/components/elements/components/forms/general/TextElementForm.vue
@@ -9,13 +9,24 @@
     <FormElement class="control">
       <HorizontalAlignmentsSelector v-model="values.alignment" />
     </FormElement>
+    <FormGroup :label="$t('textElementForm.textFormatTypeLabel')">
+      <RadioButton v-model="values.format" :value="TEXT_FORMAT_TYPES.PLAIN">
+        {{ $t('textElementForm.textFormatTypePlain') }}
+      </RadioButton>
+      <RadioButton v-model="values.format" :value="TEXT_FORMAT_TYPES.MARKDOWN">
+        {{ $t('textElementForm.textFormatTypeMarkdown') }}
+      </RadioButton>
+    </FormGroup>
   </form>
 </template>
 
 <script>
 import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
 import elementForm from '@baserow/modules/builder/mixins/elementForm'
-import { HORIZONTAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
+import {
+  HORIZONTAL_ALIGNMENTS,
+  TEXT_FORMAT_TYPES,
+} from '@baserow/modules/builder/enums'
 import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector.vue'
 
 export default {
@@ -30,8 +41,14 @@ export default {
       values: {
         value: '',
         alignment: HORIZONTAL_ALIGNMENTS.LEFT.value,
+        format: TEXT_FORMAT_TYPES.PLAIN,
       },
     }
   },
+  computed: {
+    TEXT_FORMAT_TYPES() {
+      return TEXT_FORMAT_TYPES
+    },
+  },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/page/PageContent.vue b/web-frontend/modules/builder/components/page/PageContent.vue
index 36da4f235..df22a2f88 100644
--- a/web-frontend/modules/builder/components/page/PageContent.vue
+++ b/web-frontend/modules/builder/components/page/PageContent.vue
@@ -1,19 +1,20 @@
 <template>
-  <div>
+  <ThemeProvider>
     <PageElement
       v-for="element in elements"
       :key="element.id"
       :element="element"
       :mode="mode"
     />
-  </div>
+  </ThemeProvider>
 </template>
 
 <script>
 import PageElement from '@baserow/modules/builder/components/page/PageElement'
+import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
 
 export default {
-  components: { PageElement },
+  components: { ThemeProvider, PageElement },
   inject: ['builder', 'mode'],
   props: {
     page: {
diff --git a/web-frontend/modules/builder/components/page/PagePreview.vue b/web-frontend/modules/builder/components/page/PagePreview.vue
index cceeabd9e..94d079825 100644
--- a/web-frontend/modules/builder/components/page/PagePreview.vue
+++ b/web-frontend/modules/builder/components/page/PagePreview.vue
@@ -1,5 +1,5 @@
 <template>
-  <div
+  <ThemeProvider
     class="page-preview__wrapper"
     @click.self="actionSelectElement({ element: null })"
   >
@@ -32,7 +32,7 @@
         />
       </div>
     </div>
-  </div>
+  </ThemeProvider>
 </template>
 
 <script>
@@ -42,10 +42,12 @@ import { notifyIf } from '@baserow/modules/core/utils/error'
 import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
 import { PLACEMENTS } from '@baserow/modules/builder/enums'
 import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
+import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
 
 export default {
   name: 'PagePreview',
   components: {
+    ThemeProvider,
     AddElementModal,
     ElementPreview,
     PreviewNavigationBar,
diff --git a/web-frontend/modules/builder/components/theme/ThemeProvider.vue b/web-frontend/modules/builder/components/theme/ThemeProvider.vue
new file mode 100644
index 000000000..903b87836
--- /dev/null
+++ b/web-frontend/modules/builder/components/theme/ThemeProvider.vue
@@ -0,0 +1,32 @@
+<template>
+  <div :style="style">
+    <slot></slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ThemeProvider',
+  inject: ['builder'],
+  computed: {
+    style() {
+      const colors = {
+        '--primary-color': this.builder.theme.primary_color,
+        '--secondary-color': this.builder.theme.secondary_color,
+      }
+      const headings = Array.from([1, 2, 3, 4, 5, 6]).reduce(
+        (headings, level) => ({
+          [`--heading-h${level}--font-size`]: `${
+            this.builder.theme[`heading_${level}_font_size`]
+          }px`,
+          [`--heading-h${level}--color`]:
+            this.builder.theme[`heading_${level}_color`],
+          ...headings,
+        }),
+        {}
+      )
+      return { ...colors, ...headings }
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/builder/enums.js b/web-frontend/modules/builder/enums.js
index 8c985d520..8e076d335 100644
--- a/web-frontend/modules/builder/enums.js
+++ b/web-frontend/modules/builder/enums.js
@@ -20,6 +20,11 @@ export const PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS = {
   text: ensureNonEmptyString,
 }
 
+export const TEXT_FORMAT_TYPES = {
+  PLAIN: 'plain',
+  MARKDOWN: 'markdown',
+}
+
 export const IMAGE_SOURCE_TYPES = {
   UPLOAD: 'upload',
   URL: 'url',
diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json
index 3be5a9e00..1d26c8c97 100644
--- a/web-frontend/modules/builder/locales/en.json
+++ b/web-frontend/modules/builder/locales/en.json
@@ -143,7 +143,10 @@
     "textElementForm": {
         "textTitle": "Text",
         "textPlaceholder": "Enter text...",
-        "textError": "The value is invalid."
+        "textError": "The value is invalid.",
+        "textFormatTypeLabel": "Format",
+        "textFormatTypePlain": "Plain text",
+        "textFormatTypeMarkdown": "Markdown"
     },
     "imageElement": {
         "emptyState": "No alt text defined..."
diff --git a/web-frontend/modules/core/assets/scss/components/all.scss b/web-frontend/modules/core/assets/scss/components/all.scss
index 18fcbf26b..a00836251 100644
--- a/web-frontend/modules/core/assets/scss/components/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/all.scss
@@ -19,7 +19,6 @@
 @import 'dropdown';
 @import 'field_form_context';
 @import 'tooltip';
-@import 'markdown';
 @import 'rating';
 @import 'progress';
 @import 'fields/boolean';
diff --git a/web-frontend/modules/core/assets/scss/components/api_docs.scss b/web-frontend/modules/core/assets/scss/components/api_docs.scss
index 1ea1d2688..7559f4c65 100644
--- a/web-frontend/modules/core/assets/scss/components/api_docs.scss
+++ b/web-frontend/modules/core/assets/scss/components/api_docs.scss
@@ -131,6 +131,25 @@
 
 .api-docs__content {
   color: $color-neutral-900;
+
+  &.markdown code {
+    display: inline-block;
+    font-size: 14px;
+    padding: 3px 6px;
+    background-color: $color-neutral-100;
+    @include rounded($rounded);
+  }
+
+  &.markdown p {
+    color: $color-neutral-900;
+    margin-bottom: 20px;
+    font-size: 13px;
+    line-height: 140%;
+  }
+
+  &.markdown ul {
+    padding-left: 1em;
+  }
 }
 
 .api-docs__heading-wrapper {
diff --git a/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss b/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss
index b48501565..cdbed112a 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/elements/heading_element.scss
@@ -2,7 +2,6 @@
   margin: 0;
   text-align: left;
   color: var(--color, $black);
-  font-size: var(--font-size, 30px);
 
   .element--alignment-horizontal-center & {
     text-align: center;
@@ -13,29 +12,33 @@
   }
 }
 
-// These tag specific selectors will be removed once the heading element has it's own
-// custom styling.
-h1.heading-element__heading {
-  font-size: var(--font-size, 30px);
+.ab-heading--h1 {
+  color: var(--heading-h1--color, $black);
+  font-size: var(--heading-h1--font-size, 30px);
 }
 
-h2.heading-element__heading {
-  font-size: var(--font-size, 26px);
+.ab-heading--h2 {
+  color: var(--heading-h2--color, $black);
+  font-size: var(--heading-h2--font-size, 26px);
 }
 
-h3.heading-element__heading {
-  font-size: var(--font-size, 22px);
+.ab-heading--h3 {
+  color: var(--heading-h3--color, $black);
+  font-size: var(--heading-h3--font-size, 22px);
 }
 
-h4.heading-element__heading {
-  font-size: var(--font-size, 18px);
+.ab-heading--h4 {
+  color: var(--heading-h4--color, $black);
+  font-size: var(--heading-h4--font-size, 18px);
 }
 
-h5.heading-element__heading {
-  font-size: var(--font-size, 14px);
+.ab-heading--h5 {
+  color: var(--heading-h5--color, $black);
+  font-size: var(--heading-h5--font-size, 14px);
 }
 
-h6.heading-element__heading {
-  font-size: var(--font-size, 14px);
+.ab-heading--h6 {
+  color: var(--heading-h6--color, $black);
+  font-size: var(--heading-h6--font-size, 14px);
   font-style: italic;
 }
diff --git a/web-frontend/modules/core/assets/scss/components/markdown.scss b/web-frontend/modules/core/assets/scss/components/markdown.scss
deleted file mode 100644
index bf7a55fc5..000000000
--- a/web-frontend/modules/core/assets/scss/components/markdown.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-.markdown {
-  & code {
-    display: inline-block;
-    font-size: 14px;
-    padding: 3px 6px;
-    background-color: $color-neutral-100;
-    @include rounded($rounded);
-  }
-
-  & p {
-    color: $color-neutral-900;
-    margin-bottom: 20px;
-    font-size: 13px;
-    line-height: 140%;
-  }
-
-  & ul {
-    padding-left: 1em;
-  }
-}
diff --git a/web-frontend/modules/core/components/MarkdownIt.vue b/web-frontend/modules/core/components/MarkdownIt.vue
index 68710fdba..274208c30 100644
--- a/web-frontend/modules/core/components/MarkdownIt.vue
+++ b/web-frontend/modules/core/components/MarkdownIt.vue
@@ -16,6 +16,11 @@ export default {
       type: String,
       default: 'div',
     },
+    rules: {
+      required: false,
+      type: Object,
+      default: () => {},
+    },
   },
   data() {
     return {
@@ -38,6 +43,7 @@ export default {
   async created() {
     const Markdown = (await import('markdown-it')).default
     this.md = new Markdown()
+    this.md.renderer.rules = { ...this.md.renderer.rules, ...this.rules }
     this.htmlContent = this.md.render(this.content)
   },
 }