Svelte
Svelte 5 components and runes for Vizel editor.
Installation
npm install @vizel/svelte
# or
pnpm add @vizel/svelte
# or
yarn add @vizel/svelteRequirements
- Svelte 5
Quick Start
Use the Vizel component:
<script lang="ts">
import { Vizel } from '@vizel/svelte';
import '@vizel/core/styles.css';
</script>
<Vizel
placeholder="Type '/' for commands..."
onUpdate={({ editor }) => console.log(editor.getJSON())}
/>Advanced Setup
To customize, use individual components with runes:
<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>Components
Vizel
All-in-one editor component with built-in bubble menu.
<script lang="ts">
import { Vizel } from '@vizel/svelte';
</script>
<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' },
}}
onUpdate={({ editor }) => {}}
onCreate={({ editor }) => {}}
onFocus={({ editor }) => {}}
onBlur={({ editor }) => {}}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
initialContent | JSONContent | - | Initial content (JSON) |
initialMarkdown | string | - | Initial content (Markdown) |
bind: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 |
onUpdate | Function | - | Update callback |
onCreate | Function | - | Create callback |
onFocus | Function | - | Focus callback |
onBlur | Function | - | Blur callback |
Runes
createVizelEditor
Creates and manages a Vizel editor instance using Svelte 5 runes.
<script lang="ts">
import { createVizelEditor } from '@vizel/svelte';
const editor = createVizelEditor({
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 { current: Editor | null }. Access the editor via editor.current.
createVizelState
Forces component re-render on editor state changes.
<script lang="ts">
import { createVizelState } from '@vizel/svelte';
let { editor } = $props();
// Re-renders when editor state changes
const state = createVizelState(() => editor);
</script>
{#if editor}
<div>
<span>{editor.storage.characterCount?.characters() ?? 0} characters</span>
<span>{editor.storage.characterCount?.words() ?? 0} words</span>
</div>
{/if}createVizelAutoSave
Automatically saves editor content.
<script lang="ts">
import { createVizelEditor, createVizelAutoSave, VizelEditor, VizelSaveIndicator } from '@vizel/svelte';
const editor = createVizelEditor();
const autoSave = createVizelAutoSave(() => editor.current, {
debounceMs: 2000,
storage: 'localStorage',
key: 'my-editor-content',
onSave: (content) => console.log('Saved'),
onError: (error) => console.error('Save failed', error),
});
</script>
<VizelEditor editor={editor.current} />
<VizelSaveIndicator status={autoSave.status} lastSaved={autoSave.lastSaved} />createVizelMarkdown
Two-way Markdown synchronization with debouncing.
<script lang="ts">
import { createVizelEditor, createVizelMarkdown, VizelEditor } from '@vizel/svelte';
const editor = createVizelEditor();
const md = createVizelMarkdown(() => editor.current, {
debounceMs: 300, // default: 300ms
});
</script>
<VizelEditor editor={editor.current} />
<textarea value={md.markdown} oninput={(e) => md.setMarkdown(e.target.value)} />
{#if md.isPending}
<span>Syncing...</span>
{/if}Return Value
| Property | Type | Description |
|---|---|---|
markdown | string | Current Markdown content (reactive) |
setMarkdown | (md: string) => void | Update editor from Markdown |
isPending | boolean | Whether sync is pending (reactive) |
getVizelTheme
Access theme state within VizelThemeProvider context.
<script lang="ts">
import { getVizelTheme, VizelThemeProvider } from '@vizel/svelte';
const theme = getVizelTheme();
function toggleTheme() {
theme.setTheme(theme.resolvedTheme === 'dark' ? 'light' : 'dark');
}
</script>
<VizelThemeProvider defaultTheme="system">
<Editor />
<button onclick={toggleTheme}>
{theme.resolvedTheme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</button>
</VizelThemeProvider>Components
VizelEditor
Renders the editor content area.
<VizelEditor
editor={editor.current}
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.current}
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={autoSave.status}
lastSaved={autoSave.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 lang="ts">
import { Vizel } from '@vizel/svelte';
let markdown = $state('# Hello World\n\nStart editing...');
</script>
<!-- Simple: bind:markdown for two-way binding -->
<Vizel bind:markdown={markdown} />
<!-- Or one-way with initialMarkdown -->
<Vizel initialMarkdown="# Read Only Initial" />Split View (WYSIWYG + Raw Markdown)
<script lang="ts">
import { Vizel } from '@vizel/svelte';
let markdown = $state('# Hello\n\nEdit in either pane!');
</script>
<div class="split-view">
<Vizel bind:markdown={markdown} />
<textarea bind:value={markdown} />
</div>Reactive Content with $state (JSON)
<script lang="ts">
import type { JSONContent } from '@tiptap/core';
let content = $state<JSONContent>({ type: 'doc', content: [] });
const editor = createVizelEditor({
initialContent: content,
onUpdate: ({ editor }) => {
content = editor.getJSON();
},
});
</script>With Form
<script lang="ts">
const editor = createVizelEditor();
function handleSubmit(e: Event) {
e.preventDefault();
if (editor.current) {
const content = editor.current.getJSON();
// Submit content
}
}
</script>
<form onsubmit={handleSubmit}>
<VizelEditor editor={editor.current} />
<button type="submit">Submit</button>
</form>Binding to Variable
<script lang="ts">
let editorRef = $state<Editor | null>(null);
const editor = createVizelEditor({
onCreate: ({ editor }) => {
editorRef = editor;
},
});
function focusEditor() {
editorRef?.commands.focus();
}
</script>
<button onclick={focusEditor}>Focus</button>
<VizelEditor editor={editor.current} />Custom Bubble Menu
<script lang="ts">
import type { Editor } from '@tiptap/core';
let { editor }: { editor: Editor | null } = $props();
</script>
{#if editor}
<div class="bubble-menu">
<button
onclick={() => editor.chain().focus().toggleBold().run()}
class:active={editor.isActive('bold')}
>
Bold
</button>
<button
onclick={() => editor.chain().focus().toggleItalic().run()}
class:active={editor.isActive('italic')}
>
Italic
</button>
<button onclick={() => editor.chain().focus().undo().run()}>
Undo
</button>
<button onclick={() => editor.chain().focus().redo().run()}>
Redo
</button>
</div>
{/if}Context Pattern
<!-- Parent.svelte -->
<script lang="ts">
import { setContext } from 'svelte';
import { createVizelEditor } from '@vizel/svelte';
const editor = createVizelEditor();
setContext('editor', editor);
</script>
<!-- Child.svelte -->
<script lang="ts">
import { getContext } from 'svelte';
const editor = getContext('editor');
</script>Derived State
<script lang="ts">
const editor = createVizelEditor();
createVizelState(() => editor.current);
// Derived values that update with editor state
const characterCount = $derived(
editor.current?.storage.characterCount?.characters() ?? 0
);
const wordCount = $derived(
editor.current?.storage.characterCount?.words() ?? 0
);
const isEmpty = $derived(
editor.current?.isEmpty ?? true
);
</script>
<div class="stats">
<span>{characterCount} characters</span>
<span>{wordCount} words</span>
<span>{isEmpty ? 'Empty' : 'Has content'}</span>
</div>SSR/SvelteKit Considerations
The editor is client-side only. Use browser check or onMount:
<script lang="ts">
import { browser } from '$app/environment';
import { onMount } from 'svelte';
let mounted = $state(false);
onMount(() => {
mounted = true;
});
// Only create editor on client
const editor = browser ? createVizelEditor() : { current: null };
</script>
{#if mounted}
<VizelEditor editor={editor.current} />
{:else}
<div>Loading editor...</div>
{/if}Or use dynamic import:
<script lang="ts">
import { onMount } from 'svelte';
let Editor = $state<typeof import('./Editor.svelte').default | null>(null);
onMount(async () => {
Editor = (await import('./Editor.svelte')).default;
});
</script>
{#if Editor}
<svelte:component this={Editor} />
{:else}
<div>Loading...</div>
{/if}Svelte 5 Runes vs Svelte 4
Vizel uses Svelte 5 runes. Key differences:
| Svelte 4 | Svelte 5 (Vizel) |
|---|---|
let editor | const editor = createVizelEditor() |
$: count = ... | const count = $derived(...) |
export let prop | let { prop } = $props() |
| Stores | Runes ($state, $derived) |
Next Steps
- Configuration - Editor options
- Features - Enable and configure features
- Theming - Customize appearance
- API Reference