Tech With Galvan
Back to Articles
Development4/28/2025 • 3 min read

Markdown Editor with Live Preview using Python and Streamlit

Build a Markdown editor with live preview using Python and Streamlit — split-pane editing, syntax reference, and instant HTML export.

Galvan
Galvan

Founder & Creator

Introduction

Every developer writes Markdown — READMEs, blog posts, notes — and a live-preview editor is the tool that makes it pleasant. This build gives you a split-pane editor: raw Markdown on the left, rendered output updating on the right, with a downloadable HTML export. It is one of the smallest genuinely-daily-use apps you can make with Streamlit, and it reuses the in-memory export pattern from the QR generator.

The magic ingredient is the markdown package, which converts Markdown to HTML in milliseconds — Streamlit then renders that HTML natively.

Features

  • Split-pane layout — editor left, rendered preview right.
  • Live rendering — preview updates as you type.
  • Syntax support — tables, fenced code, and strikethrough via extensions.
  • HTML export — standalone .html file with styling.
  • Sample loader — one click to see what’s possible.

Prerequisites

pip install streamlit markdown

Step 1: Create the Script

Save as md_editor.py:

import streamlit as st
import markdown

st.set_page_config(page_title="Markdown Editor", page_icon="📝")
st.title("📝 Markdown Editor")

SAMPLE = """# Hello Markdown ✨\n\nThis is **bold**, this is *italic*, this is `inline code`.\n\n## A list\n\n- First item\n- Second item\n  - Nested item\n\n## A table\n\n| Tool | Use |\n|------|-----|\n| Streamlit | UI |\n| markdown | Rendering |\n\n```python\nprint("code blocks work too")\n```\n"""

c1, c2 = st.columns([0.15, 0.85])
if c1.button("Load sample"):
    st.session_state["md_text"] = SAMPLE
if c2.button("Clear"):
    st.session_state["md_text"] = ""

text = st.text_area(
    "Markdown", value=st.session_state.get("md_text", SAMPLE),
    height=400, key="md_text", label_visibility="collapsed",
)

left, right = st.columns(2, gap="medium")
with left:
    st.markdown("**Editor**")
    st.code(text, language="markdown")
with right:
    st.markdown("**Preview**")
    html = markdown.markdown(text, extensions=["tables", "fenced_code", "sane_lists"])
    st.markdown(html, unsafe_allow_html=True)

st.download_button(
    "⬇️ Download as HTML",
    data=f"<html><body style='font-family:sans-serif;max-width:760px;margin:auto'>{html}</body></html>",
    file_name="document.html",
    mime="text/html",
)

Step 2: Run the App

streamlit run md_editor.py

Type on the left, watch the right update on every keystroke — then export a styled HTML file.

How It Works

Two renderers cooperate. The markdown library converts your text to an HTML string, with extensions unlocking features beyond core Markdown: tables for pipe tables, fenced_code for triple-backtick blocks, sane_lists for predictable numbering. Streamlit’s st.markdown(..., unsafe_allow_html=True) then displays that HTML — the flag is required because Streamlit strips raw HTML by default for safety.

The editor is a plain st.text_area bound to session_state["md_text"]. Because the sample-loader button and the text area share that key, loading a sample and typing stay in sync across reruns — the same shared-state idea the to-do list app uses for its task array.

The export wraps the converted HTML in a minimal page shell and serves it through st.download_button — no files on disk, same BytesIO thinking as every export in this series.

Common Errors & Fixes

  • Preview shows literal HTML tags — you forgot unsafe_allow_html=True, so Streamlit escaped your tags into visible text.
  • Tables render as plain pipes — the tables extension is missing from the extensions list.
  • Code blocks lose language highlighting — fenced_code extension required; highlighting itself appears only in the exported HTML when a Pygments CSS is included.
  • Text area resets after clicking Load sample twice — the button writes state but the widget has no key; both must share md_text.

Key Concepts

  • Two-stage rendering — Markdown → HTML → displayed output.
  • Extensions — opt-in features layered onto core Markdown.
  • Shared widget state — one session key syncing buttons and inputs.
  • unsafe_allow_html — opt-in HTML rendering, use with trusted content.

What to Try Next

  • Add a toolbar row — bold/italic/link buttons that insert markers into the text at the cursor.
  • Save drafts to a JSON file per document, like the to-do app’s persistence.
  • Add a word count / reading time caption computed per keystroke.
  • Export to PDF by rendering the HTML through weasyprint.

FAQ

Why not just use st.markdown on the raw text?

Streamlit’s Markdown covers headings and bold but not tables or fenced code blocks. The markdown library with extensions is a superset — and the HTML export falls out of it for free.

Is the exported HTML styled?

Minimally — it inherits browser defaults plus the inline font styling. Embed a stylesheet link (e.g. a CDN copy of GitHub’s Markdown CSS) for full styling.

Can I edit existing .md files?

Yes — add a file uploader, decode with getvalue().decode("utf-8"), and seed st.session_state["md_text"] with the contents.