Tiptap

Tiptap is an open source headless content editor and real-time collaboration framework to craft exactly the content experience you’d like to have – built for developers.

Installation

First wee need to install basic packages that we need. To make a great editor, you will need to install a whole lot more extensions. But for now, we will just install the basic packages.

npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit

Create Editor Component

Create the component Tiptap.client.vue in the components directory. The component has to be a client only component.

The one used here looks like this. Like I said earlier, you have to install a lot of packages to get the functionality you want.

<template>
  <div v-if="editor">
    <EditorContent :editor="editor" />
    <div
      class="flex flex-wrap items-center gap-1 rounded-bl-md rounded-br-md border border-input bg-transparent p-1"
    >
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().toggleBold().run()"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('bold') }"
        @click="editor.chain().focus().toggleBold().run()"
      >
        Bold
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().toggleItalic().run()"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('italic') }"
        @click="editor.chain().focus().toggleItalic().run()"
      >
        Italic
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().toggleStrike().run()"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('strike') }"
        @click="editor.chain().focus().toggleStrike().run()"
      >
        Strike
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().toggleCode().run()"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('code') }"
        @click="editor.chain().focus().toggleCode().run()"
      >
        Code
      </UiButton>
      <UiButton size="sm" variant="outline" @click="editor.chain().focus().unsetAllMarks().run()"
        >Clear marks</UiButton
      >
      <UiButton size="sm" variant="outline" @click="editor.chain().focus().clearNodes().run()"
        >Clear nodes</UiButton
      >
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('paragraph') }"
        @click="editor.chain().focus().setParagraph().run()"
      >
        Paragraph
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 1 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
      >
        H1
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 2 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
      >
        H2
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 3 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
      >
        H3
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 4 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
      >
        H4
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 5 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
      >
        H5
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('heading', { level: 6 }) }"
        @click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
      >
        H6
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('bulletList') }"
        @click="editor.chain().focus().toggleBulletList().run()"
      >
        Bullet list
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('orderedList') }"
        @click="editor.chain().focus().toggleOrderedList().run()"
      >
        Ordered list
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('codeBlock') }"
        @click="editor.chain().focus().toggleCodeBlock().run()"
      >
        Code block
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{ 'bg-primary text-primary-foreground': editor.isActive('blockquote') }"
        @click="editor.chain().focus().toggleBlockquote().run()"
      >
        Blockquote
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        @click="editor.chain().focus().setHorizontalRule().run()"
        >Horizontal rule</UiButton
      >
      <UiButton size="sm" variant="outline" @click="editor.chain().focus().setHardBreak().run()"
        >Hard break</UiButton
      >
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().undo().run()"
        @click="editor.chain().focus().undo().run()"
      >
        Undo
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :disabled="!editor.can().chain().focus().redo().run()"
        @click="editor.chain().focus().redo().run()"
      >
        Redo
      </UiButton>
      <UiButton
        size="sm"
        variant="outline"
        :class="{
          'bg-primary text-primary-foreground': editor.isActive('textStyle', { color: '#6E16B6' }),
        }"
        @click="editor.chain().focus().setColor('#6E16B6').run()"
      >
        Purple
      </UiButton>
    </div>
  </div>
</template>

<script lang="ts" setup>
  import { Color } from "@tiptap/extension-color";
  import Highlight from "@tiptap/extension-highlight";
  import Link from "@tiptap/extension-link";
  import ListItem from "@tiptap/extension-list-item";
  import SubScript from "@tiptap/extension-subscript";
  import Superscript from "@tiptap/extension-superscript";
  import Table from "@tiptap/extension-table";
  import TableCell from "@tiptap/extension-table-cell";
  import TableHeader from "@tiptap/extension-table-header";
  import TableRow from "@tiptap/extension-table-row";
  import TextStyle from "@tiptap/extension-text-style";
  import Typography from "@tiptap/extension-typography";
  import StarterKit from "@tiptap/starter-kit";
  import { EditorContent, useEditor } from "@tiptap/vue-3";

  const model = defineModel<any>({ default: "" });

  const props = withDefaults(
    defineProps<{
      modelType?: "html" | "json";
      class?: any;
    }>(),
    {
      modelType: "html",
    }
  );

  const editor = useEditor({
    content: model.value,

    editorProps: {
      attributes: {
        class:
          tw`max-h-[250px] min-h-[150px] w-full overflow-auto rounded-md rounded-bl-none rounded-br-none border border-b-0 border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50` as any,
      },
    },
    onUpdate(val) {
      if (props.modelType === "html") {
        model.value = val.editor.getHTML();
      } else if (props.modelType === "json") {
        model.value = JSON.stringify(val.editor.getJSON());
      }
    },
    extensions: [
      Color.configure({ types: [TextStyle.name, ListItem.name] }),
      TextStyle.configure({}),
      Table.configure({
        resizable: true,
      }),
      Superscript,
      SubScript,
      Link,
      Typography,
      Highlight,
      TableRow,
      TableHeader,
      StarterKit,
      TableCell,
    ],
  });
</script>

Usage

Basic

In this example, we are just passing the model to the editor. We are also customizing the look and feel of this single instance of the editor. Feel free to customize it to your liking.