React
React 19 components and hooks for Vizel editor.
Installation
npm install @vizel/react
# or
pnpm add @vizel/react
# or
yarn add @vizel/reactRequirements
- React 19, React DOM 19 (peer dependencies)
@vizel/coreand the required@tiptap/*packages are installed automatically as peer dependencies by npm 7+, pnpm, and yarn. You do not need to list them in your ownpackage.jsonunless you want to pin a specific version.- Any ESM-compatible bundler (verified against Vite 8, Next.js 15, esbuild).
Quick Start
Use the Vizel component:
import { Vizel } from '@vizel/react';
import '@vizel/core/styles.css';
function App() {
return (
<Vizel
placeholder="Type '/' for commands..."
onUpdate={({ editor }) => console.log(editor.getJSON())}
/>
);
}Advanced Setup
To customize, use individual components with hooks:
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>
);
}Components
Vizel
All-in-one editor component with built-in bubble menu.
import { Vizel } from '@vizel/react';
<Vizel
initialContent={{ type: 'doc', content: [] }}
placeholder="Start writing..."
editable={true}
autofocus="end"
showBubbleMenu={true}
enableEmbed={true}
className="my-editor"
features={{
content: {
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) |
placeholder | string | - | Placeholder text |
editable | boolean | true | Editable state |
autofocus | boolean | 'start' | 'end' | 'all' | number | - | Auto focus |
features | VizelFeatureOptions | - | Feature options |
className | string | - | CSS class |
showToolbar | boolean | false | Show fixed toolbar above editor |
showBubbleMenu | boolean | true | Show bubble menu |
enableEmbed | boolean | - | Enable embed in links |
extensions | Extensions | - | Additional Tiptap extensions |
transformDiagramsOnImport | boolean | true | Transform diagram code blocks on import |
markdown | string | - | Controlled-mode Markdown value. Pair with onMarkdownChange to drive the editor from external state. |
flavor | VizelMarkdownFlavor | "gfm" | Markdown output flavor ("commonmark", "gfm", "obsidian", "docusaurus"). |
locale | VizelLocale | - | Localized UI strings. Use createVizelLocale() to merge a partial override with the English default. |
toolbarContent | (props: { editor }) => ReactNode | - | Custom toolbar content as a render prop. The bound editor is passed in so the callback can drive Tiptap commands. Mirrors the Vue <slot name="toolbar" :editor> and Svelte toolbar: Snippet<[{ editor }]>. |
bubbleMenuContent | (props: { editor }) => ReactNode | - | Custom bubble-menu content as a render prop. Same shape as toolbarContent. |
ref | Ref<VizelRef> | - | Forwarded ref exposing the underlying Editor instance. |
onMarkdownChange | (markdown: string) => void | - | Fired when editor content changes in controlled mode (use with markdown). |
onUpdate | (props: { editor }) => void | - | Update callback |
onCreate | (props: { editor }) => void | - | Create callback |
onDestroy | () => void | - | Destroy callback |
onSelectionUpdate | (props: { editor }) => void | - | Selection change callback |
onFocus | (props: { editor }) => void | - | Focus callback |
onBlur | (props: { editor }) => void | - | Blur callback |
onError | (error: VizelError) => void | - | Fired when an editor operation surfaces a VizelError. Narrow with isVizelError(error) to inspect the structured code field. |
Hooks
useVizelEditor
This hook creates and manages a Vizel editor instance.
import { useVizelEditor } from '@vizel/react';
function Editor() {
const editor = useVizelEditor({
initialContent: { type: 'doc', content: [] },
placeholder: 'Start writing...',
features: {
content: {
mathematics: true,
},
},
onUpdate: ({ editor }) => {
console.log(editor.getJSON());
},
});
return <VizelEditor editor={editor} />;
}Options
See Editor for full options.
Return Value
Returns Editor | null. The editor instance starts as null during SSR and before initialization.
useVizelState
This hook forces a component re-render on editor state changes.
import { useVizelState } from '@vizel/react';
function EditorStats({ editor }) {
// Re-renders when editor state changes
useVizelState(editor);
if (!editor) return null;
return (
<div>
<span>{editor.storage.characterCount?.characters() ?? 0} characters</span>
<span>{editor.storage.characterCount?.words() ?? 0} words</span>
<span>{editor.isFocused ? 'Focused' : 'Blurred'}</span>
</div>
);
}useVizelEditorState
This hook returns computed editor state that updates reactively. It provides commonly needed properties like character count, word count, and undo/redo availability.
import { useVizelEditor, useVizelEditorState, VizelEditor } from '@vizel/react';
function Editor() {
const editor = useVizelEditor();
const { characterCount, wordCount, canUndo, canRedo, isFocused, isEmpty } =
useVizelEditorState(editor);
return (
<div>
<VizelEditor editor={editor} />
<div className="status-bar">
<span>{characterCount} characters</span>
<span>{wordCount} words</span>
</div>
</div>
);
}Return Value
Returns VizelEditorState:
| Property | Type | Description |
|---|---|---|
isFocused | boolean | Whether the editor is focused |
isEmpty | boolean | Whether the editor is empty |
canUndo | boolean | Whether undo is available |
canRedo | boolean | Whether redo is available |
characterCount | number | Character count |
wordCount | number | Word count |
useVizelAutoSave
This hook automatically saves editor content.
import { useVizelAutoSave, VizelEditor, VizelSaveIndicator } from '@vizel/react';
function Editor() {
const editor = useVizelEditor();
const { status, lastSaved, save, restore } = useVizelAutoSave(editor, {
debounceMs: 2000,
storage: 'localStorage',
key: 'my-editor-content',
onSave: (content) => console.log('Saved'),
onError: (error) => console.error('Save failed', error),
});
return (
<div>
<VizelEditor editor={editor} />
<VizelSaveIndicator status={status} lastSaved={lastSaved} />
</div>
);
}useVizelMarkdown
This hook provides two-way Markdown synchronization with debouncing.
import { useVizelEditor, useVizelMarkdown, VizelEditor } from '@vizel/react';
function MarkdownEditor() {
const editor = useVizelEditor();
const { markdown, setMarkdown, isPending } = useVizelMarkdown(editor, {
debounceMs: 300, // default: 300ms
});
return (
<div>
<VizelEditor editor={editor} />
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
/>
{isPending && <span>Syncing...</span>}
</div>
);
}Return Value
| Property | Type | Description |
|---|---|---|
markdown | string | Current Markdown content |
setMarkdown | (md: string) => void | Update editor from Markdown |
isPending | boolean | Whether sync is pending |
useVizelTheme
This hook accesses theme state within VizelThemeProvider.
import { useVizelTheme, VizelThemeProvider } from '@vizel/react';
function ThemeToggle() {
const { theme, setTheme } = useVizelTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
</button>
);
}
function App() {
return (
<VizelThemeProvider defaultTheme="system">
<Editor />
<ThemeToggle />
</VizelThemeProvider>
);
}Components
VizelEditor
This component renders the editor content area.
When wrapped in a VizelProvider, the editor prop is optional — the component falls back to the editor injected through context. Pass editor explicitly to bind a specific instance (useful when multiple editors live on the same page).
// With provider context
<VizelProvider editor={editor}>
<VizelEditor className="my-editor" />
</VizelProvider>
// Or explicitly
<VizelEditor editor={editor} className="my-editor" />Props
| Prop | Type | Description |
|---|---|---|
editor | Editor | null | Editor instance. Defaults to the value provided by VizelProvider when omitted. |
className | string | Custom class name |
ref | Ref<VizelEditorRef> | Forwarded ref exposing the container DOM element. |
VizelBubbleMenu
This component displays a floating bubble menu on text selection.
<VizelBubbleMenu
editor={editor}
className="my-bubble-menu"
showDefaultMenu={true}
updateDelay={100}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | - | Editor instance |
className | 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
This component provides theme context.
<VizelThemeProvider
defaultTheme="system"
storageKey="my-theme"
disableTransitionOnChange={false}
>
{children}
</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
This component displays the save status.
<VizelSaveIndicator
status={status}
lastSaved={lastSaved}
className="my-indicator"
/>VizelPortal
Render children outside the editor's DOM hierarchy, into the shared Vizel portal container at the document body level. Used internally for menus, dropdowns, and bubble menus so floating UI stacks correctly above the editor content.
<VizelPortal layer="dropdown">
<div className="my-overlay">Content</div>
</VizelPortal>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
layer | VizelPortalLayer | "dropdown" | Z-index layer ("dropdown", "popover", "modal", "toast"). |
className | string | — | Class applied to the portal wrapper element. |
disabled | boolean | false | When true, renders children in place (no portal). Useful for SSR / debugging. |
VizelIcon
This component renders an icon from the icon context. It uses Iconify icon IDs by default, and can be customized via VizelIconProvider.
import { VizelIcon } from '@vizel/react';
<VizelIcon name="bold" className="my-icon" />Patterns
Working with Markdown
Controlled vs uncontrolled
<Vizel> accepts markdown in two modes that mirror React's familiar controlled-input pattern.
| Mode | Props | Who owns the state | When to use |
|---|---|---|---|
| Uncontrolled | initialMarkdown only | The editor | Standalone editor; observe edits via onUpdate. |
| Controlled | Both markdown AND onMarkdownChange | The parent component | Sync with form state, persist to a remote store, drive the editor from another input. |
Passing only markdown without onMarkdownChange is treated as "set-once": the editor renders the initial value but later prop changes are ignored — exactly like a <textarea value="..."/> without onChange. Always pair the two props when you intend two-way sync.
import { Vizel } from '@vizel/react';
// Simple: initialMarkdown prop
function SimpleMarkdownEditor() {
return (
<Vizel
initialMarkdown="# Hello World\n\nStart editing..."
onUpdate={({ editor }) => {
const md = editor.getMarkdown();
console.log(md);
}}
/>
);
}
// Advanced: Two-way sync with useVizelMarkdown
function TwoWayMarkdownSync() {
const editor = useVizelEditor({
initialMarkdown: '# Hello',
});
const { markdown, setMarkdown, isPending } = useVizelMarkdown(editor);
return (
<div className="split-view">
<VizelEditor editor={editor} />
<div>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
/>
{isPending && <span>Syncing...</span>}
</div>
</div>
);
}Controlled Content (JSON)
function ControlledEditor() {
const [content, setContent] = useState<JSONContent>({
type: 'doc',
content: [],
});
const editor = useVizelEditor({
initialContent: content,
onUpdate: ({ editor }) => {
setContent(editor.getJSON());
},
});
return <VizelEditor editor={editor} />;
}With Form
function EditorForm() {
const editor = useVizelEditor();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (editor) {
const content = editor.getJSON();
// Submit content
}
};
return (
<form onSubmit={handleSubmit}>
<VizelEditor editor={editor} />
<button type="submit">Submit</button>
</form>
);
}With Ref
function EditorWithRef() {
const editorRef = useRef<Editor | null>(null);
const editor = useVizelEditor({
onCreate: ({ editor }) => {
editorRef.current = editor;
},
});
const focusEditor = () => {
editorRef.current?.commands.focus();
};
return (
<div>
<button onClick={focusEditor}>Focus</button>
<VizelEditor editor={editor} />
</div>
);
}Custom Bubble Menu
function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
if (!editor) return null;
return (
<div className="bubble-menu">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'active' : ''}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'active' : ''}
>
Italic
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
Undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
Redo
</button>
</div>
);
}SSR Considerations
The editor runs on the client side only. Use dynamic import or check for the browser environment:
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('./Editor'), {
ssr: false,
loading: () => <div>Loading editor...</div>,
});Or with a client boundary:
'use client';
import { VizelEditor, useVizelEditor } from '@vizel/react';
export function Editor() {
const editor = useVizelEditor();
return <VizelEditor editor={editor} />;
}Next Steps
- Editor - Editor options, features, and auto-save
- Features - Enable and configure features
- Theming - Customize appearance
- API Reference