Building a Markdown Editor with Monaco and Interactive Widgets
Markdown is great for writing, but modern web technologies enable much more - interactive charts, diagrams, and widgets embedded directly in content. This guide explores building an advanced markdown editor that goes beyond basic formatting.
The Vision
Traditional markdown editors provide plain text output. An advanced editor could offer:
- Live Preview - See rendered markdown as you type
- Data Visualization - Embed interactive charts with simple syntax
- Code Highlighting - Syntax highlighting for 100+ languages
- Diagrams - Mermaid support for flowcharts, sequences, and more
- Embeds - YouTube, GitHub Gists, CodePen, and more
- Export Options - Download as markdown or HTML
Tech Stack
Monaco Editor - VS Code's editor component React Markdown - Markdown parser with custom components ECharts - Interactive charting library Mermaid - Diagram and flowchart rendering Rehype/Remark - Markdown processing plugins
Monaco Editor Integration
Monaco is the editor that powers VS Code. Integrating it requires careful setup:
import Editor from '@monaco-editor/react';
const MarkdownEditor = () => {
return (
<Editor
height="100%"
defaultLanguage="markdown"
theme="vs-dark"
value={content}
onChange={handleChange}
options={{
minimap: { enabled: false },
fontSize: 14,
wordWrap: 'on',
lineNumbers: 'on',
quickSuggestions: true,
suggestOnTriggerCharacters: true
}}
/>
);
};Dynamic Loading
Monaco is large (~3MB). Load it dynamically to keep initial bundle small:
const Editor = dynamic(
() => import('@monaco-editor/react').then(mod => ({ default: mod.Editor })),
{
ssr: false,
loading: () => <LoadingSpinner />
}
);Custom Markdown Widgets
The magic happens in custom markdown syntax for interactive widgets:
Data Visualization
```chart
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar"],
"values": [10, 20, 15]
}
}
```This renders an interactive chart:
const ChartWidget = ({ config }) => {
const option = {
xAxis: { type: 'category', data: config.data.labels },
yAxis: { type: 'value' },
series: [{
data: config.data.values,
type: config.type
}]
};
return <ReactECharts option={option} />;
};Mermaid Diagrams
```mermaid
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Do Something]
B -->|No| D[Do Nothing]
```Renders as an interactive diagram with zoom and pan.
Timeline Widget
```timeline
{
"events": [
{"date": "2020", "title": "Started Project"},
{"date": "2023", "title": "Launched v1.0"},
{"date": "2025", "title": "Rebuilt with Next.js"}
]
}
```Custom timeline visualization with react-chrono:
const TimelineWidget = ({ events }) => {
return (
<Chrono
items={events}
mode="VERTICAL"
theme={{
primary: '#D247BF',
secondary: '#1a1a1a'
}}
/>
);
};Markdown Processing Pipeline
The rendering pipeline uses rehype and remark plugins:
const processMarkdown = (content) => {
return (
<ReactMarkdown
remarkPlugins={[
remarkGfm, // GitHub Flavored Markdown
remarkMath, // Math equations
remarkEmoji // :smile: → 😊
]}
rehypePlugins={[
rehypeSlug, // Add IDs to headings
rehypeAutolinkHeadings, // Make headings linkable
rehypeHighlight, // Code syntax highlighting
rehypeMermaid // Mermaid diagram rendering
]}
components={customComponents}
>
{content}
</ReactMarkdown>
);
};Custom Component Overrides
Override default markdown elements with custom components:
const customComponents = {
// Custom code block handler
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
// Handle special languages
if (language === 'chart') {
return <ChartWidget config={JSON.parse(children)} />;
}
if (language === 'mermaid') {
return <MermaidDiagram content={children} />;
}
// Default code highlighting
return (
<SyntaxHighlighter language={language} style={atomDark}>
{children}
</SyntaxHighlighter>
);
},
// Custom image handler with lazy loading
img({ src, alt }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={400}
loading="lazy"
className="rounded-lg"
/>
);
},
// Custom link handler (open external links in new tab)
a({ href, children }) {
const isExternal = href?.startsWith('http');
return (
<a
href={href}
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
>
{children}
</a>
);
}
};Embed Support
YouTube Embeds
[youtube](https://youtube.com/watch?v=dQw4w9WgXcQ)Automatically detects YouTube links and converts to embedded player:
const YouTubeEmbed = ({ videoId }) => {
return (
<div className="aspect-video">
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full h-full"
/>
</div>
);
};GitHub Gist Embeds
```gist
https://gist.github.com/user/abc123
```Uses script injection (iframes are blocked):
const GistEmbed = ({ user, gistId }) => {
useEffect(() => {
const script = document.createElement('script');
script.src = `https://gist.github.com/${user}/${gistId}.js`;
script.async = true;
const container = document.getElementById(`gist-${gistId}`);
if (container) {
container.appendChild(script);
}
}, [user, gistId]);
return <div id={`gist-${gistId}`} />;
};Snippet Toolbar
Quick-insert toolbar for common patterns:
const snippets = {
dataViz: {
chart: '```chart\n{\n "type": "line",\n "data": {...}\n}\n```',
timeline: '```timeline\n{\n "events": [...]\n}\n```',
gauge: '```gauge\n{\n "value": 75,\n "max": 100\n}\n```'
},
embeds: {
youtube: '[youtube](https://youtube.com/watch?v=ID)',
gist: '```gist\nhttps://gist.github.com/user/id\n```',
codepen: '[codepen](https://codepen.io/user/pen/id)'
},
markdown: {
table: '| Column 1 | Column 2 |\n|----------|----------|\n| Data 1 | Data 2 |',
task: '- [ ] Task item',
alert: '> [!NOTE]\n> Important information'
}
};Azure DevOps Wiki Syntax
Support for Azure DevOps Wiki-specific extensions:
<div class="mermaid-container">```mermaid
graph LR
A --> B
```</div>
<div class="toc-marker"></div> <!-- Table of contents -->
![[Shared Doc]] <!-- Wiki page embed -->Custom parser for these extensions:
const parseAzureWikiSyntax = (content) => {
// Convert ::: mermaid to ```mermaid
content = content.replace(/::: mermaid\n([\s\S]*?)\n:::/g, '```mermaid\n$1\n```');
// Generate TOC
content = content.replace(/\[\[_TOC_\]\]/g, generateTOC);
// Handle page embeds
content = content.replace(/!\[\[(.*?)\]\]/g, (match, page) => {
return `[${page}](/wiki/${slugify(page)})`;
});
return content;
};Export Functionality
Export as Markdown
const downloadMarkdown = () => {
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'document.md';
link.click();
URL.revokeObjectURL(url);
};Export as HTML
const downloadHTML = async () => {
const html = await renderMarkdownToHTML(content);
const template = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exported Document</title>
<style>${getStyles()}</style>
</head>
<body>
${html}
</body>
</html>
`;
const blob = new Blob([template], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'document.html';
link.click();
URL.revokeObjectURL(url);
};Performance Optimization
Debounced Preview Updates
const [content, setContent] = useState('');
const [preview, setPreview] = useState('');
const updatePreview = useMemo(
() => debounce((value) => {
setPreview(value);
}, 300),
[]
);
const handleChange = (value) => {
setContent(value);
updatePreview(value);
};Virtual Scrolling
For long documents, sync scroll position between editor and preview:
const handleEditorScroll = (editor) => {
const scrollTop = editor.getScrollTop();
const scrollHeight = editor.getScrollHeight();
const percentage = scrollTop / scrollHeight;
const preview = previewRef.current;
if (preview) {
preview.scrollTop = preview.scrollHeight * percentage;
}
};Keyboard Shortcuts
Monaco supports custom keyboard shortcuts:
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => handleSave()
);
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyP,
() => togglePreview()
);
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyB,
() => insertBold()
);Future Enhancements
Potential improvements to explore:
- Real-time Collaboration - Using Yjs or Automerge for multi-user editing
- Version History - Git-style version control for documents
- AI Assistance - LLM-powered writing suggestions and grammar checking
- Template Library - Pre-built document templates for common use cases
- Cloud Sync - Save drafts across devices with conflict resolution
Implementation Notes
A production implementation would require:
# Core dependencies
npm install @monaco-editor/react
npm install react-markdown rehype-* remark-*
npm install echarts-for-react mermaid
# Development
npm run devConclusion
Modern markdown editors demonstrate how web technologies enable professional-grade tools that once required desktop applications. Monaco Editor, WebAssembly-powered processors, and reactive frameworks combine to create powerful content creation experiences.
The technical patterns explored - custom markdown components, syntax highlighting, iframe sandboxing, and real-time preview - apply broadly to document editing, content management, and knowledge management systems.



