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.3+
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="{
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 |
showBubbleMenu | boolean | true | Show bubble menu |
enableEmbed | boolean | - | Enable embed in links |
Events
| Event | Payload | Description |
|---|---|---|
update | { editor: Editor } | Content updated |
update:markdown | string | Markdown content changed |
create | { editor: Editor } | Editor created |
focus | { editor: Editor } | Editor focused |
blur | { editor: Editor } | Editor blurred |
Composables
useVizelEditor
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: {
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 is null during SSR and before initialization.
useVizelState
Forces 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>useVizelAutoSave
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
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 | Ref<string> | Current Markdown content |
setMarkdown | (md: string) => void | Update editor from Markdown |
isPending | Ref<boolean> | Whether sync is pending |
useVizelTheme
Access theme state within VizelThemeProvider.
<script setup lang="ts">
import { useVizelTheme, VizelThemeProvider } from '@vizel/vue';
const { theme, resolvedTheme, setTheme } = useVizelTheme();
function toggleTheme() {
setTheme(resolvedTheme.value === 'dark' ? 'light' : 'dark');
}
</script>
<template>
<VizelThemeProvider defaultTheme="system">
<Editor />
<button @click="toggleTheme">
{{ resolvedTheme === 'dark' ? 'Light Mode' : 'Dark Mode' }}
</button>
</VizelThemeProvider>
</template>Components
VizelEditor
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
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
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
Displays save status.
<VizelSaveIndicator
:status="status"
:lastSaved="lastSaved"
class="my-indicator"
/>VizelPortal
Renders children in a portal.
<VizelPortal :container="document.body">
<div class="my-overlay">Content</div>
</VizelPortal>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>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 is 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
- Configuration - Editor options
- Features - Enable and configure features
- Theming - Customize appearance
- API Reference