diff --git a/frontend/src/assets/previews/reel.png b/frontend/src/assets/previews/reel.png
new file mode 100644
index 0000000000000000000000000000000000000000..092322e17ecf6ce1d002522421a428380803651f
Binary files /dev/null and b/frontend/src/assets/previews/reel.png differ
diff --git a/frontend/src/components/canvas/utils.js b/frontend/src/components/canvas/utils.js
index 49bc18776db46315476fa7b4752dbd66fee3f659..333a9d63ca85435fe0330d7903e081fe8e40758c 100644
--- a/frontend/src/components/canvas/utils.js
+++ b/frontend/src/components/canvas/utils.js
@@ -170,14 +170,24 @@ const transformHighlightedText = (
 
     if (characterIsStar) {
       highlightIsActive = !highlightIsActive;
-      text = setCharAt(text, positionWithinString, " ");
+      text = setCharAt(text, positionWithinString, (
+        (!options.invertHighlight) ?
+        // For now, just put an empty Unicode character that still registers
+        // as one. To do this properly, we would have to adjust the code so that
+        // it goes back one character. But I don't feel like doing that.
+        " " : "‎"
+      ));
     }
 
     let style = {};
 
     if (highlightIsActive || characterIsStar) {
-      style.textBackgroundColor = highlightColor;
-      style.fill = highlightedTextColor;
+      if (!options.invertHighlight) {
+        style.textBackgroundColor = highlightColor;
+        style.fill = highlightedTextColor;
+      } else {
+        style.fill = highlightColor;
+      }
     }
 
     if (
diff --git a/frontend/src/templates.js b/frontend/src/templates.js
index 1a7c5d437f3a8b0ee28fdebd00dbafb090e1734e..c59610d49ac648aa1784d408b5778bc8f72af3d9 100644
--- a/frontend/src/templates.js
+++ b/frontend/src/templates.js
@@ -10,6 +10,7 @@ import posterImage from "./assets/previews/poster.png";
 import eventPosterImage from "./assets/previews/event_poster.png";
 import regionalSuccessImage from "./assets/previews/regional_success.png";
 import socialCoverLargeTextImage from "./assets/previews/social_cover_large_text.png";
+import reelImage from "./assets/previews/reel.png";
 
 import baseEventImage from "./assets/previews/base_event.png";
 import rightEventImage from "./assets/previews/right_event.png";
@@ -174,6 +175,16 @@ const TEMPLATES = {
     },
   },
 
+  reel: {
+    name: "Reel na sociální sítě",
+    image: reelImage,
+    path: "/reel",
+    component: () => import("./views/reel/Reel.vue"),
+    meta: {
+      title: "Reel na sociální sítě",
+    },
+  },
+
   // NOTE: The following 2 templates have been disabled on request.
   // https://zulip.pirati.cz/#narrow/stream/1545-MoTo/topic/gener.C3.A1tor/near/3390404
   //
diff --git a/frontend/src/views/reel/Reel.vue b/frontend/src/views/reel/Reel.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e63f419900488a4581f72470d5141b499461f95a
--- /dev/null
+++ b/frontend/src/views/reel/Reel.vue
@@ -0,0 +1,163 @@
+<script setup>
+import { watch, ref } from "vue";
+
+import COLORS from "../../colors";
+import TEMPLATES from "../../templates";
+import DEFAULT_CONTRACTOR from "../../contractors";
+import {
+  loadFonts,
+  loadCanvasStorage,
+  setCanvasStorage,
+  updateAutoRedrawStorage,
+} from "../../utils";
+
+import Canvas from "../../components/canvas/Canvas.vue";
+import redraw from "./canvas";
+
+import Navbar from "../../components/Navbar.vue";
+import MainContainer from "../../components/MainContainer.vue";
+import ImageInput from "../../components/inputs/ImageInput.vue";
+import LongTextInput from "../../components/inputs/text/LongTextInput.vue";
+import ShortTextInput from "../../components/inputs/text/ShortTextInput.vue";
+import ReloadButton from "../../components/reload/ReloadButton.vue";
+import AutoReloadCheckbox from "../../components/reload/AutoReloadCheckbox.vue";
+</script>
+
+<script>
+await loadFonts(["12px Bebas Neue", "12px Roboto Condensed"]);
+
+export default {
+  components: {
+    Canvas,
+    Navbar,
+    MainContainer,
+    LongTextInput,
+    ShortTextInput,
+    AutoReloadCheckbox,
+    ReloadButton,
+  },
+  data() {
+    const predefinedColors = {
+      blackBackground: {
+        name: "Černý boxík, bílý text",
+        colors: {
+          background: COLORS.black,
+          mainText: COLORS.white,
+          highlightedText: COLORS.yellow1,
+          contractedByText: COLORS.black
+        },
+      },
+    };
+
+    return {
+      mainText: null,
+      mainImage: null,
+      contractedBy: DEFAULT_CONTRACTOR,
+      gradientHeightMultiplier: 1,
+      colorLabels: {
+        background: "Pozadí",
+      },
+      predefinedColors: predefinedColors,
+      colors: predefinedColors.blackBackground.colors,
+      autoRedraw: false,
+    };
+  },
+  async created() {
+    await loadCanvasStorage(this);
+  },
+  methods: {
+    async reloadCanvasProperties() {
+      const canvasProperties = {
+        mainImage: this.mainImage,
+        mainText: this.mainText,
+        contractedBy: this.contractedBy,
+        colors: this.colors,
+      };
+
+      if (canvasProperties.mainText) {
+        window.fileName = canvasProperties.mainText;
+      }
+
+      await this.$refs.canvas.redraw(canvasProperties);
+
+      delete canvasProperties.colors;
+      setCanvasStorage(canvasProperties);
+    },
+  },
+  mounted() {
+    this.$watch(
+      (vm) => [vm.mainText, vm.contractedBy, vm.colors],
+      async (value) => {
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+
+    this.$watch(
+      (vm) => [vm.autoRedraw],
+      async (value) => {
+        updateAutoRedrawStorage(this.autoRedraw);
+
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+    );
+  },
+};
+</script>
+
+<template>
+  <header>
+    <Navbar :defaultTemplate="TEMPLATES.reel"></Navbar>
+  </header>
+  <main>
+    <MainContainer>
+      <template v-slot:left>
+        <Canvas
+          ref="canvas"
+          :redrawFunction="redraw"
+          width="1080"
+          height="1920"
+        />
+      </template>
+
+      <template v-slot:right>
+        <ReloadButton :parentRefs="$refs" @click="reloadCanvasProperties" />
+        <AutoReloadCheckbox v-model="autoRedraw" />
+
+        <ImageInput
+          name="Obrázek"
+          v-model="mainImage"
+          :important="true"
+          zIndex="10"
+        />
+
+        <LongTextInput
+          name="Text"
+          v-model="mainText"
+          :important="true"
+          :highlightable="true"
+          zIndex="9"
+        />
+
+        <ShortTextInput
+          name="Zadavatel a zpracovatel"
+          v-model="contractedBy"
+          :defaultValue="DEFAULT_CONTRACTOR"
+          :important="false"
+          zIndex="4"
+        />
+      </template>
+    </MainContainer>
+  </main>
+</template>
+
+<style>
+@import "vue-select/dist/vue-select.css";
+</style>
diff --git a/frontend/src/views/reel/canvas.js b/frontend/src/views/reel/canvas.js
new file mode 100644
index 0000000000000000000000000000000000000000..33dfd31bebffe6572ec981ae87a1914fd9ae17d2
--- /dev/null
+++ b/frontend/src/views/reel/canvas.js
@@ -0,0 +1,137 @@
+import { fabric } from "fabric";
+import {
+  clearObjects,
+  sortObjects,
+  transformHighlightedText,
+  checkTextBoxHeight,
+} from "../../components/canvas/utils";
+import COLORS from "../../colors";
+import { PaddedHighlightingTextbox } from "../../components/canvas/textbox";
+
+import bgImageSource from "../../assets/template/social_cover_large_text/background.png";
+
+let mainImage = null;
+let mainImageSource = null;
+let contractedByTextbox = null;
+let mainTextBox = null;
+
+const redraw = async (canvas, options) => {
+  clearObjects([contractedByTextbox, mainTextBox], canvas);
+
+  const contractedByTextSize = Math.ceil(canvas.height * 0.01);
+  const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9);
+  const contractedByTextBottomMargin = Math.ceil(canvas.width * 0.02);
+  const contractedByTextSideMargin = Math.ceil(canvas.width * 0.03);
+
+  const mainTextMarginTop = Math.ceil(canvas.height * 0.15);
+  const mainTextMarginLeft = Math.ceil(canvas.width * 0.05);
+  const mainTextWidth = Math.ceil(canvas.width * 0.9); 
+  const mainTextSize = Math.ceil(canvas.height * 0.1);
+  const mainTextMaxLines = 7;
+  const mainTextLineHeight = 0.85;
+
+  canvas.preserveObjectStacking = true;
+
+  /* BEGIN Main image render */
+
+  if (
+    options.mainImage !== null &&
+    (!canvas.contains(mainImage) ||
+      mainImage === null ||
+      options.mainImage.src !== mainImageSource)
+  ) {
+    if (mainImage !== null) {
+      canvas.remove(mainImage);
+    }
+
+    mainImage = new fabric.Image(options.mainImage, {
+      left: 0,
+      top: 0,
+      zIndex: 0,
+    });
+
+    mainImage.controls = {
+      ...fabric.Image.prototype.controls,
+      mtr: new fabric.Control({ visible: false }),
+    };
+
+    if (mainImage.width >= mainImage.height) {
+      mainImage.scaleToHeight(canvas.height);
+    } else {
+      mainImage.scaleToWidth(canvas.width);
+    }
+
+    canvas.add(mainImage);
+    mainImageSource = options.mainImage.src;
+    // canvas.centerObject(mainImage)
+  } else if (mainImage !== null && options.mainImage === null) {
+    canvas.remove(mainImage);
+  }
+
+  /* END Main image render */
+
+  /* BEGIN Main text render */
+
+  if (options.mainText !== null) {
+    const highlightedData = transformHighlightedText(
+      options.mainText,
+      mainTextSize,
+      mainTextWidth,
+      "Bebas Neue",
+      "#fec900",
+      "#000000",
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: mainTextWidth,
+      top: mainTextMarginTop,
+      left: mainTextMarginLeft,
+      textAlign: "left",
+      fontFamily: "Bebas Neue",
+      fontSize: mainTextSize,
+      lineHeight: mainTextLineHeight,
+      fill: options.colors.mainText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 10,
+    });
+
+    checkTextBoxHeight(mainTextBox, mainTextMaxLines);
+
+    canvas.add(mainTextBox);
+  }
+
+  /* END Main text render */
+
+  /* BEGIN Contracted by render */
+
+  if (options.contractedBy !== null) {
+    contractedByTextbox = new fabric.Text(options.contractedBy, {
+      left: canvas.width - contractedByTextSideMargin,
+      top: canvas.height - contractedByTextBottomMargin - contractedByTextSize,
+      fontFamily: "Roboto Condensed",
+      fontSize: contractedByTextSize,
+      textAlign: "right",
+      fill: options.colors.contractedByText.value,
+      selectable: false,
+      zIndex: 10,
+    });
+
+    checkTextBoxHeight(contractedByTextbox, 1);
+
+    canvas.add(contractedByTextbox);
+
+    contractedByTextbox.set({
+      left: contractedByTextbox.left - contractedByTextbox.width
+    });
+    canvas.renderAll();
+  }
+
+  /* END Contracted by render */
+
+  sortObjects(canvas);
+};
+
+export default redraw;