Getting Started
Installation
Install the package for your framework:
npm install @vizel/react
# or
pnpm add @vizel/react
# or
yarn add @vizel/reactnpm install @vizel/vue
# or
pnpm add @vizel/vue
# or
yarn add @vizel/vuenpm install @vizel/svelte
# or
pnpm add @vizel/svelte
# or
yarn add @vizel/sveltePeer Dependencies
Each framework package requires its respective framework as a peer dependency:
@vizel/reactrequiresreact@^19andreact-dom@^19@vizel/vuerequiresvue@^3.4@vizel/svelterequiressvelte@^5
Quick Start
Use the Vizel component:
import { Vizel } from '@vizel/react';
import '@vizel/core/styles.css';
function App() {
return <Vizel placeholder="Type '/' for commands..." />;
}<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import '@vizel/core/styles.css';
</script>
<template>
<Vizel placeholder="Type '/' for commands..." />
</template><script lang="ts">
import { Vizel } from '@vizel/svelte';
import '@vizel/core/styles.css';
</script>
<Vizel placeholder="Type '/' for commands..." />The Vizel component includes the editor, bubble menu, and slash command menu.
Import Styles
Import the default stylesheet in your application entry point:
import '@vizel/core/styles.css';This includes both CSS variables and component styles. For custom theming, see Theming.
Advanced Usage
To customize the editor, you can use individual components:
React
import { VizelEditor, VizelBubbleMenu, useVizelEditor } from '@vizel/react';
import '@vizel/core/styles.css';
function Editor() {
const editor = useVizelEditor({
placeholder: "Type '/' for commands...",
});
return (
<div className="editor-container">
<VizelEditor editor={editor} />
{editor && <VizelBubbleMenu editor={editor} />}
</div>
);
}
export default Editor;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>Svelte
<script lang="ts">
import { VizelEditor, VizelBubbleMenu, createVizelEditor } from '@vizel/svelte';
import '@vizel/core/styles.css';
const editor = createVizelEditor({
placeholder: "Type '/' for commands...",
});
</script>
<div class="editor-container">
<VizelEditor editor={editor.current} />
{#if editor.current}
<VizelBubbleMenu editor={editor.current} />
{/if}
</div>Toolbar
You can enable the built-in fixed toolbar for a traditional formatting bar above the editor:
<Vizel showToolbar placeholder="Type '/' for commands..." /><Vizel :show-toolbar="true" placeholder="Type '/' for commands..." /><Vizel showToolbar placeholder="Type '/' for commands..." />The toolbar includes undo/redo, text formatting, headings, lists, and block actions by default. See the API reference for VizelToolbar (React, Vue, Svelte) for customization options.
Working with Content
Initial Content
You can initialize the editor with Markdown or JSON format.
Using Markdown
You can initialize with Markdown:
import { Vizel } from '@vizel/react';
<Vizel initialMarkdown="# Hello World\n\nStart editing..." /><script setup lang="ts">
import { Vizel } from '@vizel/vue';
</script>
<template>
<Vizel initialMarkdown="# Hello World\n\nStart editing..." />
</template><script lang="ts">
import { Vizel } from '@vizel/svelte';
</script>
<Vizel initialMarkdown="# Hello World\n\nStart editing..." />Or with the hook/composable/rune:
const editor = useVizelEditor({
initialMarkdown: '# Hello World\n\nStart editing...',
});const editor = useVizelEditor({
initialMarkdown: '# Hello World\n\nStart editing...',
});const editor = createVizelEditor({
initialMarkdown: '# Hello World\n\nStart editing...',
});Using JSON
You can initialize with JSON format:
const editor = useVizelEditor({
initialContent: {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Hello World' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start editing...' }],
},
],
},
});const editor = useVizelEditor({
initialContent: {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Hello World' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start editing...' }],
},
],
},
});const editor = createVizelEditor({
initialContent: {
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Hello World' }],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Start editing...' }],
},
],
},
});Getting Content
You can access the editor content in multiple formats:
// Get JSON content
const json = editor.getJSON();
// Get Markdown content
const markdown = editor.getMarkdown();
// Get HTML content
const html = editor.getHTML();
// Get plain text
const text = editor.getText();Listening to Changes
const editor = useVizelEditor({
onUpdate: ({ editor }) => {
const content = editor.getJSON();
console.log('Content updated:', content);
// Save to your backend
},
});const editor = useVizelEditor({
onUpdate: ({ editor }) => {
const content = editor.getJSON();
console.log('Content updated:', content);
// Save to your backend
},
});const editor = createVizelEditor({
onUpdate: ({ editor }) => {
const content = editor.getJSON();
console.log('Content updated:', content);
// Save to your backend
},
});Syncing Markdown Content
For two-way Markdown synchronization, use the dedicated hooks/composables/runes:
import { useVizelEditor, useVizelMarkdown, VizelEditor } from '@vizel/react';
function Editor() {
const editor = useVizelEditor();
const { markdown, setMarkdown, isPending } = useVizelMarkdown(() => editor);
// markdown updates automatically when editor content changes
// setMarkdown() updates editor content from markdown
return (
<div>
<VizelEditor editor={editor} />
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
/>
{isPending && <span>Syncing...</span>}
</div>
);
}<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import { ref } from 'vue';
const markdown = ref('# Hello World');
</script>
<template>
<!-- v-model:markdown provides two-way binding -->
<Vizel v-model:markdown="markdown" />
<textarea v-model="markdown" />
</template><script lang="ts">
import { Vizel } from '@vizel/svelte';
let markdown = $state('# Hello World');
</script>
<!-- bind:markdown provides two-way binding -->
<Vizel bind:markdown={markdown} />
<textarea bind:value={markdown} />Markdown Flavor
Vizel supports multiple Markdown output flavors. The flavor option controls how content is serialized (e.g., callout format, wiki link syntax). Input parsing is always tolerant and accepts all formats.
const editor = useVizelEditor({
flavor: 'obsidian', // 'commonmark' | 'gfm' (default) | 'obsidian' | 'docusaurus'
});const editor = useVizelEditor({
flavor: 'obsidian',
});const editor = createVizelEditor({
flavor: 'obsidian',
});See Features - Markdown Flavor Selection for details on each flavor.
Enabling Features
All features are enabled by default except collaboration, comment, mention, and wikiLink. You can disable specific features by setting them to false, or pass an options object to configure them:
const editor = useVizelEditor({
features: {
// All features below are enabled by default.
// Set to false to disable, or pass options to configure.
slashCommand: true,
table: true,
image: { onUpload: async (file) => 'https://example.com/image.png' },
codeBlock: true,
dragHandle: true,
characterCount: true,
textColor: true,
taskList: true,
link: true,
markdown: true,
mathematics: true,
embed: true,
details: true,
diagram: true,
wikiLink: true,
// Opt-in: must be explicitly enabled
comment: true,
collaboration: true,
},
});const editor = useVizelEditor({
features: {
// Disable specific features
dragHandle: false,
// Configure with options
image: { onUpload: async (file) => 'https://example.com/image.png' },
},
});const editor = createVizelEditor({
features: {
// Disable specific features
dragHandle: false,
// Configure with options
image: { onUpload: async (file) => 'https://example.com/image.png' },
},
});See Features for detailed configuration of each feature.
Composition Patterns
Vizel offers two composition patterns for integrating the editor into your application. Choose the one that best fits your needs.
Simple: All-in-One <Vizel> Component
The <Vizel> component bundles the editor, bubble menu, and slash command menu into a single component. This is the recommended approach for most use cases where you need a standard editor with minimal configuration.
When to use:
- Quick setup with sensible defaults
- Standard editor layout (editor + bubble menu)
- Configuration via props without managing the editor instance directly
import { Vizel } from '@vizel/react';
import '@vizel/core/styles.css';
function App() {
return (
<Vizel
initialMarkdown="# Hello World"
placeholder="Start writing..."
showToolbar
features={{ image: { onUpload: uploadImage } }}
onUpdate={({ editor }) => console.log(editor.getMarkdown())}
/>
);
}<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import '@vizel/core/styles.css';
</script>
<template>
<Vizel
initial-markdown="# Hello World"
placeholder="Start writing..."
:show-toolbar="true"
:features="{ image: { onUpload: uploadImage } }"
@update="({ editor }) => console.log(editor.getMarkdown())"
/>
</template><script lang="ts">
import { Vizel } from '@vizel/svelte';
import '@vizel/core/styles.css';
</script>
<Vizel
initialMarkdown="# Hello World"
placeholder="Start writing..."
showToolbar
features={{ image: { onUpload: uploadImage } }}
onUpdate={({ editor }) => console.log(editor.getMarkdown())}
/>Advanced: Decomposed Components
For full control over layout and behavior, create the editor instance yourself and compose individual components. This pattern uses VizelProvider to share the editor context with child components.
When to use:
- Custom layout (e.g., toolbar in a separate header, sidebar panels)
- Multiple editors on the same page
- Fine-grained control over which UI elements to render
- Integrating editor state into your own components via context
import {
VizelProvider,
VizelEditor,
VizelBubbleMenu,
VizelToolbar,
useVizelEditor,
} from '@vizel/react';
import '@vizel/core/styles.css';
function App() {
const editor = useVizelEditor({
placeholder: "Start writing...",
features: { image: { onUpload: uploadImage } },
onUpdate: ({ editor }) => console.log(editor.getMarkdown()),
});
return (
<VizelProvider editor={editor}>
<header>
<VizelToolbar editor={editor} />
</header>
<main>
<VizelEditor editor={editor} />
</main>
{editor && <VizelBubbleMenu editor={editor} />}
</VizelProvider>
);
}<script setup lang="ts">
import {
VizelProvider,
VizelEditor,
VizelBubbleMenu,
VizelToolbar,
useVizelEditor,
} from '@vizel/vue';
import '@vizel/core/styles.css';
const editor = useVizelEditor({
placeholder: "Start writing...",
features: { image: { onUpload: uploadImage } },
onUpdate: ({ editor }) => console.log(editor.getMarkdown()),
});
</script>
<template>
<VizelProvider :editor="editor">
<header>
<VizelToolbar :editor="editor" />
</header>
<main>
<VizelEditor :editor="editor" />
</main>
<VizelBubbleMenu v-if="editor" :editor="editor" />
</VizelProvider>
</template><script lang="ts">
import {
VizelProvider,
VizelEditor,
VizelBubbleMenu,
VizelToolbar,
createVizelEditor,
} from '@vizel/svelte';
import '@vizel/core/styles.css';
const editorState = createVizelEditor({
placeholder: "Start writing...",
features: { image: { onUpload: uploadImage } },
onUpdate: ({ editor }) => console.log(editor.getMarkdown()),
});
const editor = $derived(editorState.current);
</script>
<VizelProvider {editor}>
<header>
<VizelToolbar {editor} />
</header>
<main>
<VizelEditor {editor} />
</main>
{#if editor}
<VizelBubbleMenu {editor} />
{/if}
</VizelProvider>TIP
Components inside VizelProvider can also access the editor via the context API (useVizelContext in React/Vue, getVizelContext in Svelte) without passing the editor prop explicitly. Passing the prop directly is recommended for clarity and type safety.
Image Upload
You can configure image uploads with a custom handler:
const editor = useVizelEditor({
features: {
image: {
onUpload: async (file) => {
// Upload to your server/CDN
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
return url;
},
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
},
},
});const editor = useVizelEditor({
features: {
image: {
onUpload: async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
return url;
},
},
},
});const editor = createVizelEditor({
features: {
image: {
onUpload: async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
return url;
},
},
},
});Dark Mode
Use VizelThemeProvider for theme support:
import { VizelThemeProvider, useVizelTheme } from '@vizel/react';
function App() {
return (
<VizelThemeProvider defaultTheme="system" storageKey="my-theme">
<Editor />
<ThemeToggle />
</VizelThemeProvider>
);
}
function ThemeToggle() {
const { resolvedTheme, setTheme } = useVizelTheme();
return (
<button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
{resolvedTheme === 'dark' ? '☀️' : '🌙'}
</button>
);
}<!-- 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" storageKey="my-theme">
<Editor />
<ThemeToggle />
</VizelThemeProvider>
</template>
<!-- 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' ? '☀️' : '🌙' }}
</button>
</template><!-- App.svelte - VizelThemeProvider wraps the app -->
<script lang="ts">
import { VizelThemeProvider } from '@vizel/svelte';
import ThemeToggle from './ThemeToggle.svelte';
</script>
<VizelThemeProvider defaultTheme="system" storageKey="my-theme">
<Editor />
<ThemeToggle />
</VizelThemeProvider>
<!-- ThemeToggle.svelte - getVizelTheme() must be called inside the provider -->
<script lang="ts">
import { getVizelTheme } from '@vizel/svelte';
const theme = getVizelTheme();
function toggleTheme() {
theme.setTheme(theme.resolvedTheme === 'dark' ? 'light' : 'dark');
}
</script>
<button onclick={toggleTheme}>
{theme.resolvedTheme === 'dark' ? '☀️' : '🌙'}
</button>See Theming for more customization options.
Next Steps
- Configuration - Editor options
- Features - Configure individual features
- Theming - Customize appearance with CSS variables
- Auto-Save - Persist content automatically
- React Guide - React-specific patterns
- Vue Guide - Vue-specific patterns
- Svelte Guide - Svelte-specific patterns