Markdown Live Preview — Free HTML CSS JS Snippet

Markdown Live Preview · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Escape-then-format pipeline — every line passes through escapeHtml() before any markdown pattern is matched, making it safe to render the parser's output directly via innerHTML without risking injected markup
Chained regex inline formatting — five ordered .replace() calls handle inline code, bold, italic, and links, with code spans converted first so embedded asterisks are never mistaken for emphasis markers
Minimal block-level state machine — a single listOpen boolean and a closeList() helper are enough to correctly group consecutive list items into one <ul> and separate adjacent lists from surrounding content
Supports the markdown people actually use day to day — headings (#/##/###), bold, italic, inline code, links, bullet lists, and blockquotes, covering the core of nearly every note, README, or comment
Synchronous live re-render — a single input listener re-runs the parser and updates innerHTML on every keystroke, with no debounce needed because the regex-based parse runs in well under a millisecond
Polished split-pane styling — monospace editor font, styled headings/code/links/blockquotes in the preview, and a card layout that mirrors real documentation and CMS editors
Zero dependencies — no markdown library, parser, or build step; copy the HTML, CSS, and JS into any page and the live preview works immediately

About this UI Snippet

How this markdown live preview was built — a line-by-line regex parser with safe HTML escaping

Screenshot of the Markdown Live Preview snippet rendered live

This snippet recreates the split-pane "type markdown on the left, see formatted HTML on the right" editor found in note-taking apps, README editors, and CMS content fields — every keystroke re-renders headings, bold/italic text, inline code, links, lists, and blockquotes into live HTML. Rather than pulling in a markdown library, it implements a deliberately small parser in roughly 60 lines of vanilla JavaScript built entirely on regular expressions and string processing.

Escaping first, formatting second

Before any markdown pattern is matched, every line passes through escapeHtml(), which converts &, <, and > into their HTML entity equivalents. This single step is what makes it *safe* to drop the parser's output directly into innerHTML — without it, someone typing <script> or any HTML-looking text into the editor could have it interpreted as real markup rather than displayed as text. Escape-then-format is the standard order of operations any time user-entered text becomes rendered HTML; reversing the order (formatting first, escaping second) would corrupt the very tags the parser just generated.

Inline formatting via chained regex replacements

The inline() function runs a line through five sequential .replace() calls — one regex each for inline code (``code), bold (text), italic (*text*), and links (text) — each one wrapping its match in the corresponding HTML tag. The order matters: code spans are converted first so that asterisks *inside* a code span (like *args``) don't get mistaken for italic markers by the patterns that run afterward. This "chain of small, ordered transformations" approach is a common and effective way to handle layered text formatting without building a full tokenizer.

Line-by-line block parsing with a small state machine

render() splits the input into lines and walks them one at a time, matching each against patterns for headings (^(#{1,3})\s+(.*)), blockquotes (^>\s?(.*)), and list items (^[-*]\s+(.*)) — falling through to a plain paragraph if nothing matches. The only piece of state it tracks is a listOpen boolean: consecutive list-item lines accumulate inside one <ul>, and a closeList() helper closes that tag the moment a non-list line, blank line, or different block type appears. This tiny state machine is enough to correctly group adjacent list items into a single list — the trickiest part of line-based markdown parsing — without any lookahead or backtracking.

Live re-render on every keystroke

A single input listener calls update(), which re-runs the entire parse-and-render pipeline and replaces preview.innerHTML on every keystroke. Because the parser runs in well under a millisecond on typical note-length text, there's no need for the debouncing seen in heavier live-preview tools like the QR Code Generator — the rendering itself is cheap enough to simply run synchronously and immediately, keeping the preview perfectly in sync with the input at all times.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to trace every regex in the inline() chain by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain precisely why escapeHtml runs before any markdown pattern is matched, and why the inline code replacement happens before the bold and italic replacements rather than after. The same assistant can help optimize it, for instance asking whether the current line-by-line render() function would need restructuring to support multi-line constructs like fenced code blocks or nested lists without breaking the single listOpen boolean's state tracking. It is also useful for extending the parser: ask it to add support for numbered lists, task-list checkboxes, or tables, following the same escape-then-match-then-wrap pattern the existing code already establishes. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "markdown live preview" split-pane editor in plain HTML, CSS, and JavaScript with no markdown library or parsing dependency.

Requirements:
- A two-pane layout with a textarea for markdown input on one side and an empty container for rendered HTML output on the other, updating on every input event with no debounce.
- An escapeHtml function that converts ampersands, less-than, and greater-than characters to their HTML entity equivalents, and every line of input must pass through this function before any markdown pattern is matched against it, so that HTML-looking text typed by the user is always displayed literally rather than being interpreted as real markup when the result is assigned to innerHTML.
- An inline-formatting function that applies a fixed, ordered sequence of regex replacements on already-escaped text: inline code spans (single backticks) must be converted to code tags before bold (double asterisks) and italic (single asterisks) are processed, specifically so that literal asterisk characters inside a code span are never mistaken for emphasis markers by the patterns that run afterward. Also support markdown links in the form [text](url), rendered as anchor tags with target="_blank" and rel="noopener".
- A block-level render function that splits the input into lines and, for each line, checks in order: is it blank (close any open list), is it a heading (one to three leading hash characters), is it a blockquote (a leading greater-than sign), is it a list item (a leading dash or asterisk), falling through to a plain paragraph if none match.
- Track list state with exactly one boolean flag: opening a list tag when the first consecutive list-item line is seen, and closing it via a shared helper the moment a non-list-item line, blank line, or different block type appears, so consecutive list items group into one list and separate lists never merge together.
- The whole parse-and-render pipeline must run synchronously on every keystroke with no debounce timer, since the regex-based parsing on typical note-length text is fast enough not to need one.

Step by step

How to Use

  1. 1
    Build a two-pane editor layoutUse CSS Grid (grid-template-columns: 1fr 1fr) to place a <textarea class="md-input"> beside an empty <div class="md-preview"> that will receive the rendered HTML.
  2. 2
    Escape HTML before anything elseWrite escapeHtml(str) that replaces &, <, and > with their entity equivalents — run every line through this *first*, so any HTML-looking text the user types is displayed literally rather than executed.
  3. 3
    Chain regex replacements for inline formattingIn an inline(text) function, run the escaped text through ordered .replace() calls for inline code, bold, italic, and links — converting code spans first so embedded * characters are not mistaken for emphasis markers later in the chain.
  4. 4
    Walk lines with a small block-level state machineSplit the input on \n and match each line against heading (^#{1,3}\s+), blockquote (^>\s?), and list-item (^[-*]\s+) patterns in order, falling through to a <p> wrapper — track only one boolean, listOpen, to group consecutive list items into a shared <ul>.
  5. 5
    Close open blocks at the right momentsWrite a closeList() helper that appends </ul> and resets listOpen whenever a blank line, heading, quote, or paragraph appears after a run of list items — this is what correctly separates adjacent lists from each other.
  6. 6
    Re-render on every keystrokeAttach a single input listener to the textarea that calls update(), which re-runs render() on the current value and assigns the result to preview.innerHTML — keeping the preview perfectly in sync without any debounce, since the parse is fast enough to run synchronously.

Real-world uses

Common Use Cases

Note-taking apps and README/comment editors
Drop a lightweight markdown editor into a notes app, blog CMS, or PR/issue comment field — visitors see exactly how their formatting will render before they submit, without loading a heavyweight markdown engine.
Documentation tools and internal wikis
Give technical writers a fast, dependency-free editing experience for short-form docs, changelogs, or release notes — the supported subset (headings, lists, links, code, quotes) covers the overwhelming majority of everyday technical writing.
Teaching how markdown parsers work
A genuinely readable, line-by-line example of how block-level and inline-level parsing combine — most production markdown libraries are too large to read end to end, but this snippet demonstrates the core ideas in about 60 lines.
Comment boxes and lightweight CMS fields
Let users preview formatted comments, descriptions, or support-ticket replies as they type — reducing the "submit, see it's wrong, edit again" cycle common with plain-text inputs that render markdown server-side.
Learning safe HTML rendering from user input
A clear, minimal demonstration of the escape-before-format principle — directly transferable to any feature that converts user-entered text into rendered HTML, from chat apps to template engines.
A starting point for a custom-flavored markdown dialect
Because the parser is small and readable, it is a practical base for adding project-specific syntax — task-list checkboxes, mentions, or custom shortcodes — without inheriting a large library's configuration surface.

Got questions?

Frequently Asked Questions

Because the rendered output gets assigned directly to preview.innerHTML. If raw user input were inserted without escaping, typing something that looks like an HTML tag (e.g. <img src=x onerror=...>) could be interpreted as real markup by the browser — a classic injection risk. Running every line through escapeHtml() first converts &, <, and > into their entity forms, so any HTML-looking text displays literally as text. Only *after* that does the parser wrap matched markdown patterns in real <strong>, <code>, <a>, etc. tags — formatting that the parser itself controls and trusts.

Because code spans often contain literal asterisks — for example ` **kwargs or *args in Python documentation. If the bold/italic regexes ran first, they would match those asterisks inside the backticks and incorrectly wrap part of the code span in <strong> or <em> tags. Converting code into <code>` first removes those asterisks from consideration (they become part of an already-formed HTML tag's content) before the emphasis patterns get a chance to misfire on them.

It tracks one boolean, listOpen. When a line matches the list-item pattern (^[-*]\s+) and no list is currently open, it pushes an opening <ul> and sets listOpen = true; subsequent matching lines just become <li> entries inside that same list. The moment a line of any other type appears — a blank line, heading, quote, or paragraph — closeList() runs, appending </ul> and resetting the flag. That single piece of state is sufficient to correctly group consecutive items and keep separate lists from merging into one.

The parser is a handful of regex passes over a typical note's worth of text — it runs in well under a millisecond, far faster than a human can type. Heavier live-render operations, like the QR Code Generator's canvas redraw, debounce because *their* per-keystroke cost is high enough to cause visible stutter. Here, the cost is negligible, so the snippet keeps things simple: re-run the parser and swap innerHTML directly, keeping the preview perfectly synchronized with no added latency.

It supports the syntax used in the overwhelming majority of everyday writing: headings (#, ##, ###), bold, *italic*, inline code, links, bullet lists, and blockquotes. It deliberately omits less common features — tables, nested lists, numbered lists, images, code fences, and footnotes — to stay small, fast, and easy to read end to end. Adding any of those is a matter of inserting another pattern into the existing chain, following the same escape-then-match-then-wrap structure.

Yes — copy the HTML, CSS, and JS with the buttons on this page and use them anywhere, including commercial products, with no attribution required. It is built entirely with vanilla JavaScript regular expressions and string processing — no markdown library, parser dependency, or licensing to track.