Back to Projects Expert

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.

HTML, CSS, JavaScript ~2 hours to build Free source code

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

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="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

CSS
* { 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

JavaScript
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .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(/^&gt; (.+)$/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 Project

Single HTML file · All code included