@synclineapi/mdx-editor
v1.0.0
Published
Framework-agnostic, plugin-based MDX/Markdown editor with toolbar customization and live preview
Downloads
327
Maintainers
Readme
@synclineapi/mdx-editor
A framework-agnostic, plugin-based MDX/Markdown editor with a high-performance virtual-rendering code pane, live HTML preview, a configurable toolbar, and 33+ built-in content plugins. Works with React, Vue, Angular, Next.js, Svelte, or plain JavaScript.
Installation
npm install @synclineapi/mdx-editorQuick Start
Vanilla JavaScript
<div id="editor" style="height: 600px;"></div>
<script type="module">
import { createEditor } from '@synclineapi/mdx-editor';
import '@synclineapi/mdx-editor/style.css';
const editor = createEditor({
container: '#editor',
value: '# Hello World\n\nStart writing *MDX* here.',
onChange: (markdown) => console.log(markdown),
});
</script>React
import { useEffect, useRef } from 'react';
import { createEditor, type SynclineMDXEditor } from '@synclineapi/mdx-editor';
import '@synclineapi/mdx-editor/style.css';
export function Editor({ value, onChange }: { value: string; onChange: (v: string) => void }) {
const ref = useRef<HTMLDivElement>(null);
const editor = useRef<SynclineMDXEditor | null>(null);
useEffect(() => {
if (!ref.current) return;
editor.current = createEditor({ container: ref.current, value, onChange });
return () => editor.current?.destroy();
}, []);
return <div ref={ref} style={{ height: 600 }} />;
}Custom Plugin Selection
import { SynclineMDXEditor, boldPlugin, italicPlugin, headingPlugin } from '@synclineapi/mdx-editor';
import '@synclineapi/mdx-editor/style.css';
const editor = new SynclineMDXEditor({
container: '#editor',
plugins: [headingPlugin(), boldPlugin(), italicPlugin()],
toolbar: [['heading', '|', 'bold', 'italic']],
});Features
33+ Built-in Plugins
| Category | Plugins | |----------|---------| | Formatting | Heading (H1–H6), Bold, Italic, Strikethrough, Quote | | Links & Media | Link, Image, Image Background, Image Frame | | Code | Inline Code, Fenced Code Block | | Lists | Unordered List, Ordered List, Task List | | Layout | Table, Multi-Column (2–5 cols), Tabs, Container | | Components | Accordion, Accordion Group, Card, Card Group, Steps | | Callouts | Admonition (Tip / Warning / Caution / Danger / Info / Note), Tip (Good / Bad / Info) | | Rich Content | Highlight (8 colours), Emoji, Formula (KaTeX inline & block), Tooltip, Copy Text | | Embeds | YouTube, Video file, GitHub Gist, Twitter/X, CodePen, CodeSandbox | | Diagrams | Mermaid (Flowchart, Sequence, Class, State, ER, Journey, Gantt) | | Insert | API Endpoint, Markdown Import, Code Snippet |
Editor Engine
- Virtual rendering — only visible rows are in the DOM; handles documents of 10 000+ lines at 60 fps
- Word wrap — proper pixel-width wrap with full active-line highlight across all visual rows
- MDX autocomplete — context-aware component-tag and attribute suggestions; Tab-trigger snippet expansion
- Single-colour prose mode — all syntax tokens collapse to the text colour for distraction-free writing
- Live HTML preview — side-by-side split / editor-only / preview-only modes with a draggable splitter
- Table of Contents — auto-generated from headings, collapsible panel
- Theme system — light / dark toggle; auto-syncs with
data-theme,.smdx-dark, ordata-color-scheme
Configuration
EditorConfig
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| container | HTMLElement \| string | — | Required. DOM element or CSS selector to mount into. |
| value | string | '' | Initial markdown content. |
| onChange | (value: string) => void | — | Called on every content change (typing, paste, undo, setValue()). |
| plugins | EditorPlugin[] | all built-ins | Plugins to register. |
| toolbar | ToolbarConfig | default toolbar | Toolbar layout (rows of item IDs, groups, dividers). |
| theme | Record<string, string> | — | CSS custom property overrides for the MDX prose shell. |
| mode | 'split' \| 'editor' \| 'preview' | 'split' | Initial layout mode. |
| placeholder | string | — | Placeholder shown in an empty editor. |
| readOnly | boolean | false | Prevent all content mutations. |
| renderers | Record<string, RendererFn> | — | Custom preview renderer overrides. |
| locale | Partial<EditorLocale> | — | i18n label overrides. |
Theming
CSS custom properties
.smdx-editor {
--smdx-primary: #6366f1; /* accent colour */
--smdx-bg: #ffffff; /* prose shell background */
--smdx-text: #1e293b; /* prose text */
--smdx-border: #e2e8f0; /* dividers */
--smdx-font-family: 'Inter', sans-serif;
--smdx-font-mono: 'JetBrains Mono', monospace;
--smdx-radius: 12px;
}Via config
createEditor({
container: '#editor',
theme: {
'--smdx-primary': '#e11d48',
'--smdx-bg': '#0f172a',
},
});Dark mode
Add .smdx-dark to the editor root, any ancestor element, <body>, or <html> — the editor observes all of them. Alternatively set data-theme="dark" on any ancestor (Next.js / Nuxt / Tailwind pattern):
// CSS class toggle
editor.getRoot().classList.add('smdx-dark');
// data-theme attribute (works on any ancestor)
document.documentElement.dataset.theme = 'dark';Autocomplete
registerAutoComplete()
Add custom autocomplete items on top of the built-in MDX completions. Items appear in the popup immediately and persist for the lifetime of the editor.
import type { CompletionItem } from '@synclineapi/mdx-editor';
editor.registerAutoComplete([
// Plain word completion
{ label: 'MyCard', kind: 'cls', detail: 'component' },
// Snippet — Tab or click inserts the full body; $1 is the cursor position
{
label: 'mycard',
kind: 'snip',
detail: '<MyCard> component',
description: 'Inserts a custom card block.',
body: '<MyCard title="$1">\n $2\n</MyCard>',
},
]);Call it again at any time to add more items — they accumulate:
editor.registerAutoComplete({ label: 'MyBanner', kind: 'cls', detail: 'component' });Completion kinds
| kind | Badge | Use for |
|--------|-------|---------|
| 'cls' | C | Component names (<MyCard>, <Tabs>) |
| 'fn' | f | Functions / methods |
| 'kw' | K | Keywords |
| 'var' | · | Attributes, variables, props |
| 'typ' | T | Types / interfaces |
| 'snip' | S | Snippet template — set body to expand on accept |
Snippet bodies
Use $1, $2, … as cursor tab stops. The cursor lands on $1 after expansion:
{
label: 'endpoint',
kind: 'snip',
detail: 'API endpoint block',
body: '<Endpoint method="$1GET" path="$2/api/resource">\n $3Description\n</Endpoint>',
}Plugin API
Plugins are the recommended way to bundle a toolbar button, keyboard shortcut, preview renderer, and autocomplete items together as one reusable unit.
import type { EditorPlugin } from '@synclineapi/mdx-editor';
const myPlugin: EditorPlugin = {
name: 'my-custom-block',
// Toolbar button
toolbarItems: [{
id: 'myBlock',
label: 'My Block',
icon: '<svg>...</svg>',
tooltip: 'Insert my block (Ctrl+Shift+M)',
action: ({ editor }) => {
editor.insertBlock('<MyBlock>\n Content\n</MyBlock>');
},
}],
// Autocomplete — same content as the toolbar action, now also reachable
// by typing the trigger word in the editor
completions: [
{
label: 'myblock',
kind: 'snip',
detail: '<MyBlock> component',
body: '<MyBlock>\n $1Content\n</MyBlock>',
},
],
// Preview renderer
renderers: [{
name: 'myBlock',
pattern: /<MyBlock>([\s\S]*?)<\/MyBlock>/g,
render: (match) => {
const m = match.match(/<MyBlock>([\s\S]*?)<\/MyBlock>/);
return `<div class="my-block">${m?.[1] ?? ''}</div>`;
},
}],
shortcuts: [
{
key: 'Ctrl+Shift+m',
action: ({ editor }) => editor.insertBlock('<MyBlock>\n Content\n</MyBlock>'),
description: 'Insert custom block',
},
],
styles: `.my-block { border: 2px solid #6366f1; padding: 16px; border-radius: 8px; }`,
};You can also register completions dynamically inside init() using ctx.registerCompletion():
const myPlugin: EditorPlugin = {
name: 'dynamic-completions',
async init(ctx) {
const components = await fetchMyComponents(); // dynamic list
for (const comp of components) {
ctx.registerCompletion({
label: comp.tag,
kind: 'cls',
detail: comp.description,
});
}
},
};Completion merge order
Every time a plugin is registered or unregistered the autocomplete list is rebuilt:
built-in MDX completions (tags, JSX attributes)
+ built-in MDX snippets (toolbar-matched snippet bodies)
+ plugin completions (all registered plugins, in order)
+ user completions (registerAutoComplete() calls)Toolbar Customization
Flat toolbar
createEditor({
container: '#editor',
toolbar: [['bold', 'italic', '|', 'heading', '|', 'link', 'image']],
});Multi-row toolbar
createEditor({
container: '#editor',
toolbar: [
['heading', '|', 'bold', 'italic', 'strikethrough'],
['link', 'image', '|', 'table', 'code'],
],
});Nested dropdowns
createEditor({
container: '#editor',
toolbar: [[
'bold', 'italic', '|',
{
type: 'group',
label: 'Insert',
display: 'dropdown',
items: ['table', 'accordion', 'card', 'steps'],
},
]],
});API Reference
interface SynclineMDXEditor {
// Content
getValue(): string;
setValue(value: string): void;
// Editing
insertText(text: string): void;
insertBlock(template: string): void;
wrapSelection(prefix: string, suffix: string): void;
replaceSelection(text: string): void;
replaceCurrentLine(text: string): void;
insertAt(position: number, text: string): void;
// Selection & cursor
getSelection(): SelectionState;
setSelection(start: number, end: number): void;
getCurrentLine(): string;
getCurrentLineNumber(): number;
jumpToLine(lineNumber: number): void;
// Undo / Redo
undo(): void;
redo(): void;
// View
focus(): void;
renderPreview(): void;
getMode(): 'split' | 'editor' | 'preview';
setMode(mode: 'split' | 'editor' | 'preview'): void;
setLineNumbers(enabled: boolean): void;
// Metrics
getWordCount(): number;
getLineCount(): number;
// DOM access
getRoot(): HTMLElement;
getTextarea(): HTMLTextAreaElement;
getPreview(): HTMLElement;
// Plugins
registerPlugin(plugin: EditorPlugin): void;
unregisterPlugin(name: string): void;
// Autocomplete
registerAutoComplete(items: CompletionItem | CompletionItem[]): void;
// Theme sync
syncCodeEditorTheme(): void;
// Events
on(event: string, handler: (data?: unknown) => void): void;
off(event: string, handler: (data?: unknown) => void): void;
emit(event: string, data?: unknown): void;
// Lifecycle
destroy(): void;
}Events
onChange callback (recommended)
const editor = createEditor({
container: '#editor',
onChange: (markdown) => {
console.log('Content changed:', markdown);
},
});Event emitter
editor.on('change', (markdown) => console.log('Changed:', markdown));
editor.on('selection-change', (sel) => console.log('Selection:', sel));
editor.on('mode-change', (mode) => console.log('Mode:', mode));
editor.on('render', (html) => console.log('Preview HTML ready'));
editor.on('focus', () => console.log('Editor focused'));
editor.on('blur', () => console.log('Editor blurred'));
editor.on('toolbar-action', (id) => console.log('Toolbar clicked:', id));Bundle Size
The production build targets < 150 kB (ESM + UMD, gzipped) when mermaid, katex, and highlight.js are treated as external peer dependencies.
npm run build
npm run sizeSecurity
All rendered Markdown/MDX HTML is sanitized by a built-in XSS sanitizer before DOM injection. Dangerous tags (script, iframe, object), event-handler attributes (onclick, onerror, etc.), and javascript: protocol URLs are stripped automatically. See SECURITY.md for the vulnerability disclosure policy.
Accessibility
@synclineapi/mdx-editor targets WCAG 2.1 AA compliance:
- All toolbar buttons carry
aria-labelandtitleattributes - The editor textarea has
aria-label="Markdown editor"andaria-multiline="true" - The preview pane has
role="region",aria-label="Preview", andaria-live="polite" - Mode toggle buttons expose
aria-pressedstate - Toolbar dropdowns use
role="menu"/role="menuitem" - A skip link is provided for keyboard-only users
- All foreground/background colour pairs meet WCAG AA contrast ratios
Peer Dependencies
mermaid, katex, and highlight.js are optional. Install only the ones you need:
npm install mermaid # Mermaid diagrams
npm install katex # Math / formula rendering
npm install highlight.js # Syntax highlighting in code blocksLicense
Copyright © Syncline API. All rights reserved.
This software is proprietary and confidential. Unauthorized copying, distribution, modification, or use of this software, in whole or in part, is strictly prohibited without the express written permission of Syncline API.
