Vue
Vue 3 components and composables for Vizel editor.
Installation
npm install @vizel/vue
# or
pnpm add @vizel/vue
# or
yarn add @vizel/vueRequirements
- Vue 3.4+ (peer dependency)
@vizel/coreand the required@tiptap/*packages are installed automatically as peer dependencies by npm 7+, pnpm, and yarn. You do not need to list them in your ownpackage.jsonunless you want to pin a specific version.- Any ESM-compatible bundler (verified against Vite 8, Nuxt 3, esbuild).
Quick Start
Use the Vizel component:
<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:
<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.
<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="{
content: {
image: { onUpload: async (file) => 'url' },
},
}"
@update="({ editor }) => {}"
@create="({ editor }) => {}"
@focus="({ editor }) => {}"
@blur="({ editor }) => {}"
/>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
initialContent | JSONContent | - | Initial content (JSON) |
initialMarkdown | string | - | Initial content (Markdown) |
v-model:markdown | string | - | Two-way Markdown binding |
placeholder | string | - | Placeholder text |
editable | boolean | true | Editable state |
autofocus | boolean | 'start' | 'end' | 'all' | number | - | Auto focus |
features | VizelFeatureOptions | - | Feature options |
class | string | - | CSS class |
showToolbar | boolean | false | Show fixed toolbar above editor |
showBubbleMenu | boolean | true | Show bubble menu |
enableEmbed | boolean | - | Enable embed in links |
extensions | Extensions | - | Additional Tiptap extensions |
transformDiagramsOnImport | boolean | true | Transform diagram code blocks on import |
flavor | VizelMarkdownFlavor | "gfm" | Markdown output flavor ("commonmark", "gfm", "obsidian", "docusaurus"). |
locale | VizelLocale | - | Localized UI strings. Use createVizelLocale() to merge a partial override with the English default. |
Slots
| Slot | Slot props | Description |
|---|---|---|
toolbar | { editor } | Custom toolbar content. Renders inside <VizelToolbar> when showToolbar is true. |
bubble-menu | { editor } | Custom bubble-menu content. Renders inside <VizelBubbleMenu> when showBubbleMenu is true. |
| default | { editor } | Additional content rendered inside the editor root. |
Events
| Event | Payload | Description |
|---|---|---|
update | { editor: Editor } | Fires when content changes |
update:markdown | string | Fires when Markdown content changes (paired with v-model:markdown) |
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 |
error | VizelError | Fires when an editor operation surfaces a VizelError. Narrow with isVizelError(error) to inspect the structured code field. |
Composables
useVizelEditor
This composable creates and manages a Vizel editor instance.
<script setup lang="ts">
import { useVizelEditor } from '@vizel/vue';
const editor = useVizelEditor({
initialContent: { type: 'doc', content: [] },
placeholder: 'Start writing...',
features: {
content: {
mathematics: true,
},
},
onUpdate: ({ editor }) => {
console.log(editor.getJSON());
},
});
</script>Options
See Editor 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.
<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.
<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>:
| Property | Type | Description |
|---|---|---|
isFocused | boolean | Whether the editor is focused |
isEmpty | boolean | Whether the editor is empty |
canUndo | boolean | Whether undo is available |
canRedo | boolean | Whether redo is available |
characterCount | number | Character count |
wordCount | number | Word count |
useVizelAutoSave
This composable automatically saves editor content.
<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.
<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
| Property | Type | Description |
|---|---|---|
markdown | Readonly<ShallowRef<string>> | Current Markdown content. Templates auto-unwrap the ref; in <script setup> read markdown.value. |
setMarkdown | (md: string) => void | Update editor from Markdown |
isPending | ComputedRef<boolean> | Whether sync is pending |
flush | () => void | Force-flush pending updates immediately |
useVizelTheme
This composable accesses theme state within VizelThemeProvider.
<!-- 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><!-- ThemeToggle.vue - useVizelTheme() must be called inside the provider -->
<script setup lang="ts">
import { useVizelTheme } from '@vizel/vue';
const { theme, setTheme } = useVizelTheme();
function toggleTheme() {
setTheme(theme.value === 'dark' ? 'light' : 'dark');
}
</script>
<template>
<button @click="toggleTheme">
{{ theme === 'dark' ? 'Light Mode' : 'Dark Mode' }}
</button>
</template>Components
VizelEditor
This component renders the editor content area.
<VizelEditor
:editor="editor"
class="my-editor"
/>Props
| Prop | Type | Description |
|---|---|---|
editor | Editor | null | Editor instance |
class | string | Custom class name |
VizelBubbleMenu
This component displays a floating bubble menu on text selection.
<VizelBubbleMenu
:editor="editor"
class="my-bubble-menu"
:showDefaultMenu="true"
:updateDelay="100"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | - | Editor instance |
class | string | - | Custom class name |
showDefaultMenu | boolean | true | Show default bubble menu |
pluginKey | string | "vizelBubbleMenu" | Plugin key |
updateDelay | number | 100 | Position update delay |
shouldShow | Function | - | Custom visibility logic |
enableEmbed | boolean | - | Enable embed in link editor |
VizelThemeProvider
This component provides theme context.
<VizelThemeProvider
defaultTheme="system"
storageKey="my-theme"
:disableTransitionOnChange="false"
>
<slot />
</VizelThemeProvider>Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultTheme | "light" | "dark" | "system" | "system" | Default theme |
storageKey | string | "vizel-theme" | Storage key |
targetSelector | string | - | Theme attribute target |
disableTransitionOnChange | boolean | false | Disable transitions |
VizelSaveIndicator
This component displays the save status.
<VizelSaveIndicator
:status="status"
:lastSaved="lastSaved"
class="my-indicator"
/>VizelPortal
Render children outside the editor's DOM hierarchy, into the shared Vizel portal container at the document body level. Used internally for menus, dropdowns, and bubble menus so floating UI stacks correctly above the editor content.
<VizelPortal layer="dropdown">
<div class="my-overlay">Content</div>
</VizelPortal>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
layer | VizelPortalLayer | "dropdown" | Z-index layer ("dropdown", "popover", "modal", "toast"). |
class | string | — | Class applied to the portal wrapper element. |
disabled | boolean | false | When true, renders children in place (no portal). Useful for SSR / debugging. |
VizelIcon
This component renders an icon from the icon context. It uses Iconify icon IDs by default, and can be customized via VizelIconProvider.
<script setup lang="ts">
import { VizelIcon } from '@vizel/vue';
</script>
<template>
<VizelIcon name="bold" class="my-icon" />
</template>Patterns
Working with Markdown
<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>Controlled vs uncontrolled
| Mode | Props | Who owns the state | When to use |
|---|---|---|---|
| Uncontrolled | :initial-markdown="..." only | The editor | Standalone editor; observe edits via @update. |
| Controlled | v-model:markdown="..." (or :markdown + @update:markdown) | The parent component | Sync with form state, persist to a remote store, drive the editor from another input. |
Passing only :markdown without listening for update:markdown is treated as "set-once": the editor renders the initial value but later prop changes are ignored. Always use v-model:markdown (or both halves manually) when you intend two-way sync.
Split View (WYSIWYG + Raw Markdown)
<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)
<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
<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
<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
<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
<!-- 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:
<template>
<ClientOnly>
<Editor />
<template #fallback>
<div>Loading editor...</div>
</template>
</ClientOnly>
</template>Or use dynamic import:
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const Editor = defineAsyncComponent(() => import('./Editor.vue'));
</script>Next Steps
- Editor - Editor options, features, and auto-save
- Features - Enable and configure features
- Theming - Customize appearance
- API Reference