A block-based visual editor for Markdown built with Tiptap, supporting React, Vue, and Svelte.
/ to insert blocks[[page-name]] syntax for internal linking (opt-in)| Package | Description |
|---|---|
@vizel/core |
Framework-agnostic core with Tiptap extensions and styles |
@vizel/react |
React 19 components and hooks |
@vizel/vue |
Vue 3 components and composables |
@vizel/svelte |
Svelte 5 components and runes |
# React
npm install @vizel/react
# Vue
npm install @vizel/vue
# Svelte
npm install @vizel/svelte
Install additional packages to enable optional features:
| Feature | Package | Install |
|---|---|---|
| Mathematics (LaTeX) | katex |
npm install katex |
| Diagrams (Mermaid) | mermaid |
npm install mermaid |
| Diagrams (GraphViz) | @hpcc-js/wasm-graphviz |
npm install @hpcc-js/wasm-graphviz |
| Code syntax highlighting | lowlight |
npm install lowlight |
When using mathematics, also import the KaTeX stylesheet:
import '@vizel/core/styles.css';
import '@vizel/core/mathematics.css'; // KaTeX styles for math rendering
Vizel uses OKLCH color values by default, which are compatible with shadcn/ui's theming system. For shadcn/ui projects, import components.css (without CSS variable definitions) and map your theme colors:
import '@vizel/core/components.css';
:root {
/* Map your shadcn colors to Vizel */
--vizel-primary: var(--primary);
--vizel-background: var(--background);
--vizel-foreground: var(--foreground);
--vizel-border: var(--border);
--vizel-muted: var(--muted);
--vizel-accent: var(--accent);
}
The Vizel component is the recommended way to get started. It includes a floating bubble menu on text selection, with an optional fixed toolbar.
import { Vizel } from '@vizel/react';
import '@vizel/core/styles.css';
function App() {
return (
<Vizel
placeholder="Type '/' for commands..."
onUpdate={({ editor }) => console.log(editor.getJSON())}
/>
);
}
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import '@vizel/core/styles.css';
function handleUpdate({ editor }: { editor: any }) {
console.log(editor.getJSON());
}
</script>
<template>
<Vizel
placeholder="Type '/' for commands..."
@update="handleUpdate"
/>
</template>
<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())}
/>
For more control, use individual components with hooks/composables/runes:
// React
import { VizelEditor, VizelBubbleMenu, useVizelEditor } from '@vizel/react';
import '@vizel/core/styles.css';
function Editor() {
const editor = useVizelEditor({
placeholder: "Type '/' for commands...",
features: {
image: {
onUpload: async (file) => 'https://example.com/image.png',
},
},
});
return (
<div>
<VizelEditor editor={editor} />
{editor && <VizelBubbleMenu editor={editor} />}
</div>
);
}
<!-- Vue -->
<script setup lang="ts">
import { VizelEditor, VizelBubbleMenu, useVizelEditor } from '@vizel/vue';
import '@vizel/core/styles.css';
const editor = useVizelEditor({
placeholder: "Type '/' for commands...",
features: {
image: {
onUpload: async (file) => 'https://example.com/image.png',
},
},
});
</script>
<template>
<div>
<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...",
features: {
image: {
onUpload: async (file) => 'https://example.com/image.png',
},
},
});
</script>
<VizelEditor editor={editor.current} />
{#if editor.current}
<VizelBubbleMenu editor={editor.current} />
{/if}
| Prop | Type | Default | Description |
|---|---|---|---|
initialContent |
JSONContent |
- | Initial editor content |
initialMarkdown |
string |
- | Initial content in Markdown format |
placeholder |
string |
- | Placeholder text |
editable |
boolean |
true |
Whether editor is editable |
autofocus |
boolean | 'start' | 'end' | 'all' | number |
- | Auto focus behavior |
features |
VizelFeatureOptions |
- | Feature configuration |
class / className |
string |
- | Custom CSS class |
showToolbar |
boolean |
false |
Show fixed toolbar above editor |
showBubbleMenu |
boolean |
true |
Show bubble menu on selection |
enableEmbed |
boolean |
- | Enable oEmbed/OGP embed previews in the link editor |
transformDiagramsOnImport |
boolean |
- | Transform diagrams on Markdown import |
extensions |
Extensions |
- | Additional Tiptap extensions |
onCreate |
(props) => void |
- | Called when editor is created |
onUpdate |
(props) => void |
- | Called on content update |
onSelectionUpdate |
(props) => void |
- | Called on selection change |
onFocus |
(props) => void |
- | Called when editor gains focus |
onBlur |
(props) => void |
- | Called when editor loses focus |
onDestroy |
() => void |
- | Called when editor is destroyed |
The onError callback is available through useVizelEditor (React), useVizelEditor (Vue), or createVizelEditor (Svelte). It is not a prop on the Vizel all-in-one component.
const editor = useVizelEditor({
onError: (error) => {
console.error(`[${error.code}] ${error.message}`);
},
});
Note: After the callback is invoked, the error is re-thrown. Use this callback for logging or user notifications.
All major features are enabled by default except collaboration, comment, and wikiLink:
// Using Vizel component
<Vizel
placeholder="Type '/' for commands..."
features={{
// Image upload (configure handler)
image: {
onUpload: async (file) => 'url',
maxFileSize: 10 * 1024 * 1024, // 10MB
},
}}
/>
// Or using useVizelEditor hook
const editor = useVizelEditor({
features: {
slashCommand: true, // enabled by default
table: true, // enabled by default
link: true, // enabled by default
taskList: true, // enabled by default
textColor: true, // enabled by default
codeBlock: true, // enabled by default
dragHandle: true, // enabled by default
characterCount: true, // enabled by default
markdown: true, // enabled by default
mathematics: true, // enabled by default
embed: true, // enabled by default
details: true, // enabled by default
diagram: true, // enabled by default
wikiLink: true, // opt-in: must be explicitly enabled
comment: true, // opt-in: must be explicitly enabled
// collaboration: true, // opt-in: must be explicitly enabled
},
});
// React
import { useVizelAutoSave } from '@vizel/react';
const { status, lastSaved } = useVizelAutoSave(() => editor, {
debounceMs: 2000,
storage: 'localStorage', // or 'sessionStorage' or custom
key: 'my-editor-content',
});
// Vue
import { useVizelAutoSave } from '@vizel/vue';
const { status, lastSaved } = useVizelAutoSave(() => editor.value, {
debounceMs: 2000,
storage: 'localStorage',
key: 'my-editor-content',
});
// Svelte
import { createVizelAutoSave } from '@vizel/svelte';
const autoSave = createVizelAutoSave(() => editor.current, {
debounceMs: 2000,
storage: 'localStorage',
key: 'my-editor-content',
});
// Access: autoSave.status, autoSave.lastSaved
// React
import { VizelThemeProvider, useVizelTheme } from '@vizel/react';
function App() {
return (
<VizelThemeProvider defaultTheme="system" storageKey="my-theme">
<Editor />
</VizelThemeProvider>
);
}
function ThemeToggle() {
const { resolvedTheme, setTheme } = useVizelTheme();
return (
<button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
Toggle
</button>
);
}
Import the pre-built stylesheet (includes CSS variables + component styles):
import '@vizel/core/styles.css';
For shadcn/ui projects, use components-only styles (without CSS variable definitions):
import '@vizel/core/components.css';
Override CSS variables to customize the appearance:
:root {
--vizel-primary: #3b82f6;
--vizel-background: #ffffff;
--vizel-foreground: #111827;
--vizel-border: #e5e7eb;
--vizel-radius-md: 0.375rem;
}
[data-vizel-theme="dark"] {
--vizel-primary: #60a5fa;
--vizel-background: #1f2937;
--vizel-foreground: #f9fafb;
--vizel-border: #374151;
}
| Category | Variables |
|---|---|
| Colors | --vizel-primary, --vizel-primary-hover, --vizel-background, --vizel-foreground, --vizel-border, --vizel-muted, --vizel-accent, --vizel-success, --vizel-warning, --vizel-error |
| Typography | --vizel-font-sans, --vizel-font-mono, --vizel-font-size-sm, --vizel-font-size-base, --vizel-line-height-normal |
| Spacing | --vizel-spacing-1 through --vizel-spacing-12 |
| Border Radius | --vizel-radius-sm, --vizel-radius-md, --vizel-radius-lg, --vizel-radius-full |
| Shadows | --vizel-shadow-sm, --vizel-shadow-md, --vizel-shadow-lg |
| Transitions | --vizel-transition-fast, --vizel-transition-normal, --vizel-transition-slow |
| Editor | --vizel-editor-min-height, --vizel-editor-padding, --vizel-editor-font-family |
| Code Block | --vizel-code-block-bg, --vizel-code-block-text, --vizel-code-block-border |
See _tokens.scss for all available design tokens and _variables.scss for how they're output as CSS variables.
# Install dependencies
pnpm install
# Run demos
pnpm dev:react # React demo (http://localhost:3000)
pnpm dev:vue # Vue demo (http://localhost:3001)
pnpm dev:svelte # Svelte demo (http://localhost:3002)
pnpm dev:all # All demos simultaneously
# Build all packages
pnpm build
# Type check
pnpm typecheck
# Lint
pnpm lint
pnpm check # Lint + format check
# Run E2E tests
pnpm test:ct # All frameworks (parallel)
pnpm test:ct:react # React only
pnpm test:ct:vue # Vue only
pnpm test:ct:svelte # Svelte only
MIT