Markdown Editor
Build a live Markdown Editor that converts your text to HTML in real time. Learn regex-based parsing, split-pane layout, localStorage, and sanitized DOM injection - all in pure JavaScript.
What You’ll Learn
- How to parse Markdown to HTML using regular expressions
- How to safely inject generated HTML into the DOM with
innerHTML - How to build a split-pane editor with synchronized scroll
- How to save and restore editor content with
localStorage - How to handle real-time input events and render previews on every keystroke
How It Works
Every keystroke triggers a render
We attach an input event listener to the textarea. On every change, the raw Markdown string is passed through a chain of replace() calls that convert syntax like **text**, # Heading, and - item into their HTML equivalents.
Regex rules transform Markdown tokens
Each Markdown rule is a single replace() with a regex pattern. Headings (# to ######), bold, italic, inline code, links, blockquotes, horizontal rules, and unordered lists are all handled sequentially — order matters because patterns can overlap.
innerHTML renders the live preview
The parsed HTML string is assigned directly to preview.innerHTML. Because we control the input (no external user URLs without validation), this is safe in this learning context. The preview panel scrolls independently so you always see what you’re typing.
localStorage persists your work
On every input event we also call localStorage.setItem() to save the raw Markdown. When the page loads, we read that value back and populate the editor — so your work survives a refresh.
Source Code
HTML Structure
<!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="style.css">
</head>
<body>
<div class="app">
<header class="app-header">
<h1>Markdown Editor</h1>
<div class="header-actions">
<button id="clearBtn" class="action-btn danger">Clear</button>
<span id="wordCount" class="word-count">0 words</span>
</div>
</header>
<div class="editor-wrap">
<div class="pane">
<div class="pane-label">Markdown</div>
<textarea id="editor" placeholder="Type your Markdown here..."></textarea>
</div>
<div class="pane">
<div class="pane-label">Preview</div>
<div id="preview" class="preview"></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
CSS Styling
* { margin:0; padding:0; box-sizing:border-box; }
body { min-height:100vh; background:linear-gradient(135deg,#1a1a2e,#2d1b69);
font-family:'Segoe UI',sans-serif; display:flex; align-items:flex-start;
justify-content:center; padding:20px 10px; }
.app { background:#fff; border-radius:20px; padding:24px; width:100%;
max-width:900px; box-shadow:0 20px 60px rgba(0,0,0,0.4); }
.app-header { display:flex; align-items:center; justify-content:space-between;
margin-bottom:16px; }
h1 { font-size:1.4rem; color:#1a1a2e; }
.header-actions { display:flex; align-items:center; gap:12px; }
.action-btn { padding:6px 16px; border:none; border-radius:8px; font-size:.82rem;
font-weight:600; cursor:pointer; transition:all .2s; }
.action-btn.danger { background:#fee2e2; color:#dc2626; }
.action-btn.danger:hover { background:#dc2626; color:#fff; }
.word-count { font-size:.8rem; color:#999; }
.editor-wrap { display:flex; gap:12px; height:500px; }
.pane { flex:1; display:flex; flex-direction:column; border:1px solid #e0e0e0;
border-radius:12px; overflow:hidden; }
.pane-label { background:#f4f6fb; padding:8px 14px; font-size:.75rem; font-weight:700;
color:#555; letter-spacing:.5px; text-transform:uppercase;
border-bottom:1px solid #e0e0e0; }
textarea { flex:1; padding:16px; font-family:'Courier New',monospace; font-size:.9rem;
line-height:1.7; border:none; outline:none; resize:none;
background:#fafafa; color:#333; }
.preview { flex:1; padding:16px; overflow-y:auto; font-size:.92rem;
line-height:1.75; color:#333; }
.preview h1 { font-size:1.6rem; font-weight:800; color:#1a1a2e; margin:0 0 12px; }
.preview h2 { font-size:1.35rem; font-weight:700; color:#1a1a2e; margin:16px 0 8px; }
.preview h3 { font-size:1.1rem; font-weight:700; color:#1a1a2e; margin:14px 0 6px; }
.preview p { margin:0 0 12px; }
.preview strong { font-weight:700; }
.preview em { font-style:italic; }
.preview code { background:#f0f0f0; padding:2px 6px; border-radius:4px;
font-family:'Courier New',monospace; font-size:.85em; color:#8e44ad; }
.preview blockquote { border-left:4px solid #8e44ad; margin:12px 0;
padding:8px 14px; background:#f9f4ff; color:#666; font-style:italic; }
.preview ul { padding-left:22px; margin:0 0 12px; }
.preview li { margin-bottom:4px; }
.preview a { color:#8e44ad; text-decoration:underline; }
.preview hr { border:none; border-top:2px solid #e0e0e0; margin:16px 0; }
@media(max-width:640px) { .editor-wrap { flex-direction:column; height:auto; }
.pane { height:280px; } }
JavaScript Logic
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const wordCount = document.getElementById('wordCount');
const clearBtn = document.getElementById('clearBtn');
const defaultMarkdown = `# Welcome to Markdown Editor
Type in the left pane and see the **live preview** on the right.
## Features
- **Bold** with \`**text**\`
- *Italic* with \`*text*\`
- \`Inline code\` with backticks
- [Links](https://example.com) with \`[text](url)\`
> Blockquotes start with \`>\`
---
Paragraphs are separated by blank lines.`;
function parseMarkdown(md) {
return md
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/^###### (.+)$/gm, '<h6>$1</h6>')
.replace(/^##### (.+)$/gm, '<h5>$1</h5>')
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
.replace(/^---$/gm, '<hr>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
.replace(/\n\n/g, '</p><p>')
.replace(/^(?!<[hH1-6uo]|<li|<block|<hr)(.+)$/gm, '$1')
.replace(/^<\/p><p>$/gm, '')
.replace(/<p><\/p>/g, '');
}
function render() {
const md = editor.value;
preview.innerHTML = parseMarkdown(md);
const words = md.trim() ? md.trim().split(/\s+/).length : 0;
wordCount.textContent = words + ' word' + (words !== 1 ? 's' : '');
localStorage.setItem('md-editor-content', md);
}
editor.addEventListener('input', render);
clearBtn.addEventListener('click', () => {
editor.value = '';
localStorage.removeItem('md-editor-content');
render();
});
const saved = localStorage.getItem('md-editor-content');
editor.value = saved !== null ? saved : defaultMarkdown;
render();
Download Source Code
Single ready-to-run HTML file. Open it in any browser!
Download ProjectSingle HTML file · All code included