Skip to content

Getting Started

Installation

Install the package for your framework:

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

Dependencies and peer dependencies

Framework packages (@vizel/react / @vizel/vue / @vizel/svelte) are thin adapters over @vizel/core. @vizel/core and @vizel/headless are regular dependencies of each adapter, so the package manager installs them automatically when you add the adapter. You never list @vizel/core or @vizel/headless in your own package.json.

@vizel/core still appears in style imports (for example import "@vizel/core/styles.css") because CSS cannot be re-exported across packages. The import resolves through the transitively installed package.

The genuine peer dependencies are @tiptap/* and the framework runtime. The package manager installs these automatically (npm 7+, pnpm, and yarn), and you list them explicitly only when you want to pin a specific version:

  • @vizel/react requires react@^19, react-dom@^19, and @tiptap/pm
  • @vizel/vue requires vue@^3.4
  • @vizel/svelte requires svelte@^5

Optional features (lowlight for code highlighting, katex for math, mermaid / @hpcc-js/wasm-graphviz for diagrams, yjs + y-websocket for collaboration) are declared as optional peers. Install them only when you enable the corresponding feature.

Node.js toolchain requirement

Vizel declares engines.node as >=24. This floor is advisory toolchain guidance for two audiences: contributors who build the monorepo, and the Continuous Integration (CI) pipeline that tests and publishes the packages. The repository pins Node.js 24 in .nvmrc and .node-version.

The engines.node floor does not gate the Node.js runtime of a consumer application that installs @vizel/react, @vizel/vue, or @vizel/svelte. Your app chooses its own Node.js version; npm emits a warning rather than an error when the app's Node.js version falls below the declared floor. The published packages ship as standard ECMAScript Modules (ESM) that bundlers and runtimes resolve without a Node.js 24 minimum at install time. See ADR-0015.

Quick Start

Use the Vizel component:

tsx
import { Vizel } from '@vizel/react';
import '@vizel/core/styles.css';

function App() {
  return <Vizel placeholder="Type '/' for commands..." />;
}
vue
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import '@vizel/core/styles.css';
</script>

<template>
  <Vizel placeholder="Type '/' for commands..." />
</template>
svelte
<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:

typescript
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

tsx
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

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

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:

tsx
<Vizel showToolbar placeholder="Type '/' for commands..." />
vue
<Vizel :show-toolbar="true" placeholder="Type '/' for commands..." />
svelte
<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:

tsx
import { Vizel } from '@vizel/react';

<Vizel initialMarkdown="# Hello World\n\nStart editing..." />
vue
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
</script>

<template>
  <Vizel initialMarkdown="# Hello World\n\nStart editing..." />
</template>
svelte
<script lang="ts">
import { Vizel } from '@vizel/svelte';
</script>

<Vizel initialMarkdown="# Hello World\n\nStart editing..." />

Or with the hook/composable/rune:

tsx
const editor = useVizelEditor({
  initialMarkdown: '# Hello World\n\nStart editing...',
});
vue
const editor = useVizelEditor({
  initialMarkdown: '# Hello World\n\nStart editing...',
});
svelte
const editor = createVizelEditor({
  initialMarkdown: '# Hello World\n\nStart editing...',
});

Using JSON

You can initialize with JSON format:

tsx
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...' }],
      },
    ],
  },
});
vue
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...' }],
      },
    ],
  },
});
svelte
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:

typescript
// 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

tsx
const editor = useVizelEditor({
  onUpdate: ({ editor }) => {
    const content = editor.getJSON();
    console.log('Content updated:', content);
    // Save to your backend
  },
});
vue
const editor = useVizelEditor({
  onUpdate: ({ editor }) => {
    const content = editor.getJSON();
    console.log('Content updated:', content);
    // Save to your backend
  },
});
svelte
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:

tsx
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>
  );
}
vue
<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>
svelte
<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.

tsx
const editor = useVizelEditor({
  flavor: 'obsidian', // 'commonmark' | 'gfm' (default) | 'obsidian' | 'docusaurus'
});
vue
const editor = useVizelEditor({
  flavor: 'obsidian',
});
svelte
const editor = createVizelEditor({
  flavor: 'obsidian',
});

See Markdown - Flavors for details on each flavor.

Enabling Features

VizelFeatureOptions is grouped into three categories: content (what the document can contain), interaction (how the user edits), and collaboration (who edits together). Set any leaf to false to disable, or pass an options object to configure. Use vizelDefaultFeatures() to enable the curated default set without listing every key.

tsx
const editor = useVizelEditor({
  features: {
    content: {
      // Enabled by default; set to false to disable, or pass options to configure.
      table: true,
      image: { onUpload: async (file) => 'https://example.com/image.png' },
      mathematics: true,
      embed: true,
      details: true,
      diagram: true,
      callout: true,
      textColor: true,
      taskList: true,
      // Opt-in: must be explicitly enabled.
      wikiLink: true,
    },
    interaction: {
      dragHandle: true,
      characterCount: true,
      typography: true,
      // Opt-in: requires consumer-supplied items.
      mention: { items: async (query) => [] },
    },
    collaboration: {
      // Opt-in: comments require a provider.
      provider: true,
      comments: true,
    },
  },
});
vue
const editor = useVizelEditor({
  features: {
    content: {
      image: { onUpload: async (file) => 'https://example.com/image.png' },
    },
    interaction: {
      dragHandle: false,
    },
  },
});
svelte
const editor = createVizelEditor({
  features: {
    content: {
      image: { onUpload: async (file) => 'https://example.com/image.png' },
    },
    interaction: {
      dragHandle: false,
    },
  },
});

See Editor - Features for the feature catalog and per-feature options.

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
tsx
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())}
    />
  );
}
vue
<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>
svelte
<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
tsx
import {
  VizelProvider,
  VizelEditor,
  VizelBubbleMenu,
  VizelToolbar,
  useVizelEditor,
} from '@vizel/react';
import '@vizel/core/styles.css';

function App() {
  const editor = useVizelEditor({
    placeholder: "Start writing...",
    features: { content: { 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>
  );
}
vue
<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: { content: { 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>
svelte
<script lang="ts">
import {
  VizelProvider,
  VizelEditor,
  VizelBubbleMenu,
  VizelToolbar,
  createVizelEditor,
} from '@vizel/svelte';
import '@vizel/core/styles.css';

const editorState = createVizelEditor({
  placeholder: "Start writing...",
  features: { content: { 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:

tsx
const editor = useVizelEditor({
  features: {
    content: {
      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'],
      },
    },
  },
});
vue
const editor = useVizelEditor({
  features: {
    content: {
      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;
        },
      },
    },
  },
});
svelte
const editor = createVizelEditor({
  features: {
    content: {
      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:

tsx
import { VizelThemeProvider, useVizelTheme } from '@vizel/react';

function App() {
  return (
    <VizelThemeProvider defaultTheme="system" storageKey="my-theme">
      <Editor />
      <ThemeToggle />
    </VizelThemeProvider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useVizelTheme();

  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
}
vue
<!-- 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 { theme, setTheme } = useVizelTheme();

function toggleTheme() {
  setTheme(theme.value === 'dark' ? 'light' : 'dark');
}
</script>

<template>
  <button @click="toggleTheme">
    {{ theme === 'dark' ? '☀️' : '🌙' }}
  </button>
</template>
svelte
<!-- 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.current === 'dark' ? 'light' : 'dark');
  }
</script>

<button onclick={toggleTheme}>
  {theme.current === 'dark' ? '☀️' : '🌙'}
</button>

See Theming for more customization options.

Next Steps

  • Editor - Editor options, features, lifecycle, and auto-save
  • Blocks - Block selection, clipboard, drag handle, and block menu
  • Markdown - Flavor system, encoding modes, and round-trip
  • Theming - Customize appearance with CSS variables
  • SSR - Server-side rendering and static HTML generation
  • Collaboration - Real-time editing, comments, and version history

Released under the MIT License.