Skip to content

Svelte

Svelte 5 components and runes for Vizel editor.

Installation

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

Requirements

  • Svelte 5

Quick Start

Use the Vizel component:

svelte
<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:

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>

Components

Vizel

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

svelte
<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

PropTypeDefaultDescription
initialContentJSONContent-Initial content (JSON)
initialMarkdownstring-Initial content (Markdown)
bind:markdownstring-Two-way Markdown binding
placeholderstring-Placeholder text
editablebooleantrueEditable state
autofocusboolean | 'start' | 'end' | 'all' | number-Auto focus
featuresVizelFeatureOptions-Feature options
classstring-CSS class
showBubbleMenubooleantrueShow bubble menu
enableEmbedboolean-Enable embed in links
onUpdateFunction-Update callback
onCreateFunction-Create callback
onFocusFunction-Focus callback
onBlurFunction-Blur callback

Runes

createVizelEditor

Creates and manages a Vizel editor instance using Svelte 5 runes.

svelte
<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.

svelte
<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.

svelte
<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.

svelte
<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

PropertyTypeDescription
markdownstringCurrent Markdown content (reactive)
setMarkdown(md: string) => voidUpdate editor from Markdown
isPendingbooleanWhether sync is pending (reactive)

getVizelTheme

Access theme state within VizelThemeProvider context.

svelte
<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.

svelte
<VizelEditor 
  editor={editor.current} 
  class="my-editor"
/>

Props

PropTypeDescription
editorEditor | nullEditor instance
classstringCustom class name

VizelBubbleMenu

Floating bubble menu on text selection.

svelte
<VizelBubbleMenu 
  editor={editor.current}
  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

Provides theme context.

svelte
<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

Displays save status.

svelte
<VizelSaveIndicator 
  status={autoSave.status} 
  lastSaved={autoSave.lastSaved}
  class="my-indicator"
/>

VizelPortal

Renders children in a portal.

svelte
<VizelPortal container={document.body}>
  <div class="my-overlay">Content</div>
</VizelPortal>

Patterns

Working with Markdown

svelte
<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)

svelte
<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)

svelte
<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

svelte
<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

svelte
<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

svelte
<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

svelte
<!-- 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

svelte
<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:

svelte
<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:

svelte
<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 4Svelte 5 (Vizel)
let editorconst editor = createVizelEditor()
$: count = ...const count = $derived(...)
export let proplet { prop } = $props()
StoresRunes ($state, $derived)

Next Steps

Released under the MIT License.