Skip to content

Vue

Vue 3 components and composables for Vizel editor.

Installation

bash
npm install @vizel/vue
# or
pnpm add @vizel/vue
# or
yarn add @vizel/vue

Requirements

  • Vue 3.4+

Quick Start

Use the Vizel component:

vue
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import '@vizel/core/styles.css';
</script>

<template>
  <Vizel
    placeholder="Type '/' for commands..."
    @update="({ editor }) => console.log(editor.getJSON())"
  />
</template>

Advanced Setup

To customize, use individual components with composables:

vue
<script setup lang="ts">
import { VizelEditor, VizelBubbleMenu, useVizelEditor } from '@vizel/vue';
import '@vizel/core/styles.css';

const editor = useVizelEditor({
  placeholder: "Type '/' for commands...",
});
</script>

<template>
  <div class="editor-container">
    <VizelEditor :editor="editor" />
    <VizelBubbleMenu v-if="editor" :editor="editor" />
  </div>
</template>

Components

Vizel

All-in-one editor component with built-in bubble menu.

vue
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
</script>

<template>
  <Vizel
    :initialContent="{ type: 'doc', content: [] }"
    placeholder="Start writing..."
    :editable="true"
    autofocus="end"
    :showBubbleMenu="true"
    :enableEmbed="true"
    class="my-editor"
    :features="{
      image: { onUpload: async (file) => 'url' },
    }"
    @update="({ editor }) => {}"
    @create="({ editor }) => {}"
    @focus="({ editor }) => {}"
    @blur="({ editor }) => {}"
  />
</template>

Props

PropTypeDefaultDescription
initialContentJSONContent-Initial content (JSON)
initialMarkdownstring-Initial content (Markdown)
v-model:markdownstring-Two-way Markdown binding
placeholderstring-Placeholder text
editablebooleantrueEditable state
autofocusboolean | 'start' | 'end' | 'all' | number-Auto focus
featuresVizelFeatureOptions-Feature options
classstring-CSS class
showToolbarbooleanfalseShow fixed toolbar above editor
showBubbleMenubooleantrueShow bubble menu
enableEmbedboolean-Enable embed in links
extensionsExtensions-Additional Tiptap extensions
transformDiagramsOnImportbooleantrueTransform diagram code blocks on import

Events

EventPayloadDescription
update{ editor: Editor }Fires when content changes
update:markdownstringFires when Markdown content changes
create{ editor: Editor }Fires when the editor initializes
destroy-Fires when the editor destroys
selectionUpdate{ editor: Editor }Fires when the selection changes
focus{ editor: Editor }Fires when the editor gains focus
blur{ editor: Editor }Fires when the editor loses focus

Composables

useVizelEditor

This composable creates and manages a Vizel editor instance.

vue
<script setup lang="ts">
import { useVizelEditor } from '@vizel/vue';

const editor = useVizelEditor({
  initialContent: { type: 'doc', content: [] },
  placeholder: 'Start writing...',
  features: {
    markdown: true,
    mathematics: true,
  },
  onUpdate: ({ editor }) => {
    console.log(editor.getJSON());
  },
});
</script>

Options

See Configuration for full options.

Return Value

Returns ShallowRef<Editor | null>. The editor starts as null during SSR and before initialization.

useVizelState

This composable forces a component re-render on editor state changes.

vue
<script setup lang="ts">
import { useVizelState } from '@vizel/vue';

const props = defineProps<{ editor: Editor | null }>();

// Re-renders when editor state changes
useVizelState(() => props.editor);
</script>

<template>
  <div v-if="editor">
    <span>{{ editor.storage.characterCount?.characters() ?? 0 }} characters</span>
    <span>{{ editor.storage.characterCount?.words() ?? 0 }} words</span>
  </div>
</template>

useVizelEditorState

This composable returns computed editor state that updates reactively. It provides commonly needed properties like character count, word count, and undo/redo availability.

vue
<script setup lang="ts">
import { useVizelEditor, useVizelEditorState, VizelEditor } from '@vizel/vue';

const editor = useVizelEditor();
const editorState = useVizelEditorState(() => editor.value);
</script>

<template>
  <VizelEditor :editor="editor" />
  <div class="status-bar">
    <span>{{ editorState.characterCount }} characters</span>
    <span>{{ editorState.wordCount }} words</span>
  </div>
</template>

Return Value

Returns ComputedRef<VizelEditorState>:

PropertyTypeDescription
isFocusedbooleanWhether the editor is focused
isEmptybooleanWhether the editor is empty
canUndobooleanWhether undo is available
canRedobooleanWhether redo is available
characterCountnumberCharacter count
wordCountnumberWord count

useVizelAutoSave

This composable automatically saves editor content.

vue
<script setup lang="ts">
import { useVizelEditor, useVizelAutoSave, VizelEditor, VizelSaveIndicator } from '@vizel/vue';

const editor = useVizelEditor();

const { status, lastSaved, save, restore } = useVizelAutoSave(() => editor.value, {
  debounceMs: 2000,
  storage: 'localStorage',
  key: 'my-editor-content',
  onSave: (content) => console.log('Saved'),
  onError: (error) => console.error('Save failed', error),
});
</script>

<template>
  <div>
    <VizelEditor :editor="editor" />
    <VizelSaveIndicator :status="status" :lastSaved="lastSaved" />
  </div>
</template>

useVizelMarkdown

This composable provides two-way Markdown synchronization with debouncing.

vue
<script setup lang="ts">
import { useVizelEditor, useVizelMarkdown, VizelEditor } from '@vizel/vue';

const editor = useVizelEditor();
const { markdown, setMarkdown, isPending } = useVizelMarkdown(() => editor.value, {
  debounceMs: 300, // default: 300ms
});
</script>

<template>
  <VizelEditor :editor="editor" />
  <textarea :value="markdown" @input="setMarkdown($event.target.value)" />
  <span v-if="isPending">Syncing...</span>
</template>

Return Value

PropertyTypeDescription
markdownRef<string>Current Markdown content
setMarkdown(md: string) => voidUpdate editor from Markdown
isPendingRef<boolean>Whether sync is pending

useVizelTheme

This composable accesses theme state within VizelThemeProvider.

vue
<!-- App.vue - VizelThemeProvider wraps the app -->
<script setup lang="ts">
import { VizelThemeProvider } from '@vizel/vue';
import ThemeToggle from './ThemeToggle.vue';
</script>

<template>
  <VizelThemeProvider defaultTheme="system">
    <Editor />
    <ThemeToggle />
  </VizelThemeProvider>
</template>
vue
<!-- ThemeToggle.vue - useVizelTheme() must be called inside the provider -->
<script setup lang="ts">
import { useVizelTheme } from '@vizel/vue';

const { resolvedTheme, setTheme } = useVizelTheme();

function toggleTheme() {
  setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
}
</script>

<template>
  <button @click="toggleTheme">
    {{ resolvedTheme === 'dark' ? 'Light Mode' : 'Dark Mode' }}
  </button>
</template>

Components

VizelEditor

This component renders the editor content area.

vue
<VizelEditor 
  :editor="editor" 
  class="my-editor"
/>

Props

PropTypeDescription
editorEditor | nullEditor instance
classstringCustom class name

VizelBubbleMenu

This component displays a floating bubble menu on text selection.

vue
<VizelBubbleMenu 
  :editor="editor"
  class="my-bubble-menu"
  :showDefaultMenu="true"
  :updateDelay="100"
/>

Props

PropTypeDefaultDescription
editorEditor | null-Editor instance
classstring-Custom class name
showDefaultMenubooleantrueShow default bubble menu
pluginKeystring"vizelBubbleMenu"Plugin key
updateDelaynumber100Position update delay
shouldShowFunction-Custom visibility logic
enableEmbedboolean-Enable embed in link editor

VizelThemeProvider

This component provides theme context.

vue
<VizelThemeProvider 
  defaultTheme="system"
  storageKey="my-theme"
  :disableTransitionOnChange="false"
>
  <slot />
</VizelThemeProvider>

Props

PropTypeDefaultDescription
defaultTheme"light" | "dark" | "system""system"Default theme
storageKeystring"vizel-theme"Storage key
targetSelectorstring-Theme attribute target
disableTransitionOnChangebooleanfalseDisable transitions

VizelSaveIndicator

This component displays the save status.

vue
<VizelSaveIndicator 
  :status="status" 
  :lastSaved="lastSaved"
  class="my-indicator"
/>

VizelPortal

This component renders children in a portal.

vue
<VizelPortal :container="document.body">
  <div class="my-overlay">Content</div>
</VizelPortal>

VizelIcon

This component renders an icon from the icon context. It uses Iconify icon IDs by default, and can be customized via VizelIconProvider.

vue
<script setup lang="ts">
import { VizelIcon } from '@vizel/vue';
</script>

<template>
  <VizelIcon name="bold" class="my-icon" />
</template>

Patterns

Working with Markdown

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Vizel } from '@vizel/vue';

const markdown = ref('# Hello World\n\nStart editing...');
</script>

<template>
  <!-- Simple: v-model:markdown for two-way binding -->
  <Vizel v-model:markdown="markdown" />
  
  <!-- Or one-way with initialMarkdown -->
  <Vizel initialMarkdown="# Read Only Initial" />
</template>

Split View (WYSIWYG + Raw Markdown)

vue
<script setup lang="ts">
import { ref } from 'vue';
import { Vizel } from '@vizel/vue';

const markdown = ref('# Hello\n\nEdit in either pane!');
</script>

<template>
  <div class="split-view">
    <Vizel v-model:markdown="markdown" />
    <textarea v-model="markdown" />
  </div>
</template>

Controlled Content with v-model (JSON)

vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { JSONContent } from '@tiptap/core';

const content = ref<JSONContent>({ type: 'doc', content: [] });

const editor = useVizelEditor({
  initialContent: content.value,
  onUpdate: ({ editor }) => {
    content.value = editor.getJSON();
  },
});
</script>

With Form

vue
<script setup lang="ts">
const editor = useVizelEditor();

function handleSubmit() {
  if (editor.value) {
    const content = editor.value.getJSON();
    // Submit content
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <VizelEditor :editor="editor" />
    <button type="submit">Submit</button>
  </form>
</template>

Template Ref Access

vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue';

const editorRef = ref<Editor | null>(null);

const editor = useVizelEditor({
  onCreate: ({ editor }) => {
    editorRef.value = editor;
  },
});

function focusEditor() {
  editorRef.value?.commands.focus();
}
</script>

<template>
  <div>
    <button @click="focusEditor">Focus</button>
    <VizelEditor :editor="editor" />
  </div>
</template>

Custom Bubble Menu

vue
<script setup lang="ts">
import type { Editor } from '@tiptap/core';

const props = defineProps<{ editor: Editor | null }>();
</script>

<template>
  <div v-if="editor" class="bubble-menu">
    <button
      @click="editor.chain().focus().toggleBold().run()"
      :class="{ active: editor.isActive('bold') }"
    >
      Bold
    </button>
    <button
      @click="editor.chain().focus().toggleItalic().run()"
      :class="{ active: editor.isActive('italic') }"
    >
      Italic
    </button>
    <button @click="editor.chain().focus().undo().run()">
      Undo
    </button>
    <button @click="editor.chain().focus().redo().run()">
      Redo
    </button>
  </div>
</template>

Provide/Inject Pattern

vue
<!-- Parent.vue -->
<script setup lang="ts">
import { provide } from 'vue';
import { useVizelEditor } from '@vizel/vue';

const editor = useVizelEditor();
provide('editor', editor);
</script>

<!-- Child.vue -->
<script setup lang="ts">
import { inject, type ShallowRef } from 'vue';
import type { Editor } from '@tiptap/core';

const editor = inject<ShallowRef<Editor | null>>('editor');
</script>

SSR/Nuxt Considerations

The editor runs on the client side only. Use <ClientOnly> in Nuxt:

vue
<template>
  <ClientOnly>
    <Editor />
    <template #fallback>
      <div>Loading editor...</div>
    </template>
  </ClientOnly>
</template>

Or use dynamic import:

vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

const Editor = defineAsyncComponent(() => import('./Editor.vue'));
</script>

Next Steps

Released under the MIT License.