Build a Markdown Editor in JavaScript
Introduction
Markdown editors are essential tools for developers, writers, and content creators. They combine the simplicity of plain text with powerful formatting capabilities.
In this tutorial, you'll build a feature-rich Markdown editor with live preview, toolbar buttons, and local storage support.
What You'll Build
- Split-pane editor (write/preview)
- Live Markdown rendering
- Formatting toolbar
- Auto-save to local storage
- Export functionality
What You'll Learn
- Markdown parsing with marked.js
- DOM manipulation
- Local storage API
- Real-time preview
- Event handling
What is Markdown?
Markdown is a lightweight markup language that allows you to format text using simple syntax.
Common Markdown Syntax
# Heading 1
## Heading 2
**Bold text**
*Italic text*
- Bullet point
- Another point
[Link text](https://example.com)
```
Code block
```
> Blockquote
Why Markdown?
- Easy to learn and read
- Converts to clean HTML
- Used by GitHub, Reddit, Stack Overflow
- Perfect for documentation
Project Overview
Our Markdown editor will feature:
- Split-screen layout (editor | preview)
- Real-time Markdown conversion
- Toolbar with common formatting buttons
- Auto-save functionality
- Export to HTML/Markdown file
Project Setup
Bash
# Create project folder
mkdir markdown-editor
cd markdown-editor
# Create files
touch index.html styles.css app.js
We'll use a CDN for the Markdown library:
- marked.js - Converts Markdown to HTML
- highlight.js - Code syntax highlighting (optional)
HTML Structure
Let's build the editor interface.
HTML
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Editor</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="editor-container">
<!-- Toolbar -->
<div class="toolbar">
<button data-action="bold" title="Bold">B</button>
<button data-action="italic" title="Italic">I</button>
<button data-action="heading" title="Heading">H</button>
<button data-action="link" title="Link">🔗</button>
<button data-action="code" title="Code"><></button>
<button data-action="quote" title="Quote">"</button>
<button data-action="list" title="List">☰</button>
<div class="toolbar-spacer"></div>
<button data-action="download-md" title="Download MD">↓ MD</button>
<button data-action="download-html" title="Download HTML">↓ HTML</button>
</div>
<!-- Editor Area -->
<div class="editor-pane">
<textarea id="editor" placeholder="Type your markdown here..."># Welcome to Markdown Editor
Start typing to see your **formatted** text appear in real-time!
## Features
- Live preview
- Toolbar buttons
- Auto-save
```javascript
console.log('Code highlighting!');
```</textarea>
</div>
<!-- Preview Area -->
<div class="preview-pane">
<div class="preview-label">Preview</div>
<div id="preview"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="app.js"></script>
</body>
</html>
Markdown Parsing
Now let's add the JavaScript to convert Markdown to HTML.
JavaScript
// app.js
// DOM Elements
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
// Configure marked.js
marked.setOptions({
breaks: true, // Convert \n to <br>
gfm: true // GitHub Flavored Markdown
});
// Convert markdown to HTML
function updatePreview() {
const markdown = editor.value;
const html = marked.parse(markdown);
preview.innerHTML = html;
}
// Event listener for input
editor.addEventListener('input', updatePreview);
// Initial render
updatePreview();
marked.js Features
- GFM: Tables, task lists, strikethrough
- Sanitize: Can filter HTML for security
- Custom rendering: Override element rendering
Live Preview Enhancement
Let's make the preview update smoothly with scroll sync.
JavaScript
// Live preview with debouncing
let debounceTimer;
function debounce(func, wait) {
return function(...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, args), wait);
};
}
// Update preview with debounce (300ms delay)
const debouncedUpdate = debounce(updatePreview, 300);
editor.addEventListener('input', debouncedUpdate);
// Syntax highlighting for code blocks
function highlightCode() {
preview.querySelectorAll('pre code').forEach((block) => {
block.classList.add('language-javascript');
// In production, use highlight.js here
});
}
// Add after updatePreview
function updatePreview() {
const markdown = editor.value;
const html = marked.parse(markdown);
preview.innerHTML = html;
highlightCode();
}
Adding Toolbar
Let's make the toolbar buttons insert Markdown syntax.
JavaScript
// Toolbar functionality
const toolbarButtons = document.querySelectorAll('[data-action]');
// Insert text at cursor position
function insertText(before, after = '') {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
// Build new text
const newText = before + selectedText + after;
// Replace selection
editor.value =
editor.value.substring(0, start) +
newText +
editor.value.substring(end);
// Move cursor
editor.selectionStart = start + before.length;
editor.selectionEnd = start + before.length + selectedText.length;
editor.focus();
// Update preview
updatePreview();
}
// Toolbar button handlers
toolbarButtons.forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
switch(action) {
case 'bold':
insertText('**', '**');
break;
case 'italic':
insertText('*', '*');
break;
case 'heading':
insertText('## ');
break;
case 'link':
insertText('[', '](url)');
break;
case 'code':
insertText('`', '`');
break;
case 'quote':
insertText('> ');
break;
case 'list':
insertText('- ');
break;
case 'download-md':
downloadFile('document.md', editor.value, 'text/markdown');
break;
case 'download-html':
const htmlContent = `<!DOCTYPE html>
<html>
<head><title>Document</title>
<body>${marked.parse(editor.value)}</body>
</html>`;
downloadFile('document.html', htmlContent, 'text/html');
break;
}
});
});
// Download helper
function downloadFile(filename, content, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
Local Storage
Let's add auto-save functionality.
JavaScript
// Auto-save to local storage
const STORAGE_KEY = 'markdown-editor-content';
// Save content
function saveContent() {
localStorage.setItem(STORAGE_KEY, editor.value);
}
// Load saved content
function loadContent() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
editor.value = saved;
updatePreview();
}
}
// Save on input (debounced)
editor.addEventListener('input', debounce(saveContent, 1000));
// Load on page load
loadContent();
// Optional: Clear saved content
// localStorage.removeItem(STORAGE_KEY);
Summary
Congratulations! You've built a complete Markdown editor.
What You Built
- Split-pane Editor - Write and preview side by side
- Live Preview - Real-time Markdown to HTML conversion
- Toolbar - Quick formatting buttons
- Auto-save - Local storage persistence
- Export - Download as MD or HTML
Key Concepts Learned
- Markdown parsing with marked.js
- Text manipulation in textareas
- Debouncing for performance
- Local storage API
- Blob and download API
Enhancements to Try
- Add syntax highlighting with highlight.js
- Implement image upload
- Add keyboard shortcuts
- Create dark/light themes
- Add table support