A block-based visual editor for Markdown built with Tiptap, supporting React, Vue, and Svelte.
/ to insert blocks| 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
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 wraps the editor content with a floating toolbar out of the box.
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';
</script>
<template>
<Vizel
placeholder="Type '/' for commands..."
@update="({ editor }) => console.log(editor.getJSON())"
/>
</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>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
initialContent |
JSONContent |
- | Initial editor content |
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 |
showBubbleMenu |
boolean |
true |
Show bubble menu on selection |
enableEmbed |
boolean |
- | Enable embed in bubble menu link editor |
All major features are enabled by default:
// 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
},
});
// 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
npm install
# Run demos
npm run dev:react # React demo (http://localhost:3000)
npm run dev:vue # Vue demo (http://localhost:3001)
npm run dev:svelte # Svelte demo (http://localhost:3002)
npm run dev:all # All demos simultaneously
# Build all packages
npm run build
# Type check
npm run typecheck
# Lint
npm run lint
npm run check # Lint + format with auto-fix
# Run E2E tests
npm run test:ct # All frameworks (parallel)
npm run test:ct:react # React only
npm run test:ct:vue # Vue only
npm run test:ct:svelte # Svelte only
MIT