The Markdown preview that follows your cursor, and lets you click back. One small Rust binary. No Node.js, no Deno, works offline.

Requires Neovim 0.9 or later. Vim is not supported.
With lazy.nvim:
{
"ywsrock/nvim-md-preview",
build = ":MdPreviewInstall",
config = function()
require("md_preview").setup({})
end,
}
Open a Markdown file, or any text file, and run :MarkdownPreview. That's it. The plugin only defines its commands at startup; the rest loads when you use them.
" vim-plug: then run :MdPreviewInstall once
Plug 'ywsrock/nvim-md-preview'
-- packer.nvim
use { "ywsrock/nvim-md-preview", run = ":MdPreviewInstall" }
:MdPreviewInstall downloads the binary that matches the plugin version (plus pandoc and Typst for export) in parallel with progress on the command line. If you update the plugin without a build hook, the preview warns when the binary is out of date; run :MdPreviewInstall again. Something wrong? Run :checkhealth md_preview.
| nvim-md-preview | markdown-preview.nvim | peek.nvim | |
|---|---|---|---|
| Runtime | Single Rust binary | Node.js app (prebuilt bundle or build with yarn) | Deno |
| Works in Vim | No | Yes | No |
| Preview follows the cursor | Block by block | Yes | Yes |
| Click the preview to jump in Neovim | Yes | No | No |
| Export to PDF / DOCX / HTML / EPUB | Built in | Browser print | No |
| Git diff of the document | Yes | No | No |
| Slides from the same file | Yes | No | No |
In-buffer renderers such as render-markdown.nvim and markview.nvim solve a different problem. Use them while editing and nvim-md-preview when you want the real page, diagrams and math, or an exported file. They work well together.
This table reflects our understanding as of October 2026. If something is wrong, please open an issue.
The preview keeps the block under your cursor in view, using source-line markers instead of guessing from the scroll ratio. Click a paragraph in the preview and Neovim moves there. Local .md links and [[wiki links]] open in Neovim too.
Mermaid diagrams, KaTeX math and syntax highlighting for 190 languages are built into the binary, so nothing is fetched from the network.
PlantUML blocks (```plantuml or ```puml) are drawn with your local plantuml command when it is installed. To use a PlantUML server instead, set plantuml_server; diagram sources are sent to that server only when you set it.
Tables and math. GitHub-flavored tables with alignment, inline and display math.
Code, callouts and tasks. GitHub alerts and Obsidian callouts, including foldable ones, task lists and footnotes.
Changes against HEAD are shown as soon as you edit a file in a Git repository, unsaved edits included. :MdPreviewDiff main compares with a branch instead, :MdPreviewDiff staged with the index, and :MdPreviewDiff off hides it. Changed words are underlined or struck through in place, tables and math show the previous version in a collapsed box, removed paragraphs stay visible, and ↑ / ↓ in the header jump from change to change. The markers beside the scrollbar show where the changes are.
Markdown, vimwiki, Quarto, R Markdown and MDX files are rendered as documents. Every other text file is shown as highlighted code with line numbers, and scroll sync, click-to-jump, search and the Git diff work the same way. Help, file explorers, terminals and other special buffers are skipped, and so are binary files and files over 2 MB. Limit the file types with filetypes, for example { "markdown", "vimwiki" }.
Open the table of contents with t and search with / or Ctrl+F. The current section is highlighted as you scroll.
Slides. Press p to present the document, split on --- or on headings. The slide follows your cursor. :MdPreviewExport slides saves them as a PDF, one slide per page.
Print preview. See the document on an A4 sheet with page-break guides, then print from the browser.
:MdPreviewExport pdf writes a PDF next to your file that looks exactly like the preview: diagrams, math, callouts and highlighted code included. It prints the preview with the Chrome, Edge, Brave or Chromium already on your machine, in the background, with paper size, margins, font, a table of contents and numbered headings from your settings. DOCX, HTML and EPUB go through pandoc. Unsaved edits are included.
[!NOTE] Without one of those browsers, PDFs are made with Typst instead (
:checkhealth md_previewtells you which one is used). Typst output supports custom templates, but does not render Mermaid diagrams, math or callouts.
{
"ywsrock/nvim-md-preview",
lazy = false, -- keep the commands available in every buffer (keys alone would lazy-load)
build = ":MdPreviewInstall",
config = function() require("md_preview").setup({}) end,
keys = {
{ "<leader>mp", "<cmd>MarkdownPreviewToggle<cr>", desc = "Markdown: preview" },
{ "<leader>md", "<cmd>MdPreviewDiff<cr>", desc = "Markdown: Git diff" },
{
"<leader>me",
function()
vim.ui.select({ "pdf", "docx", "html", "epub" }, { prompt = "Export as" }, function(format)
if format then vim.cmd("MdPreviewExport " .. format) end
end)
end,
desc = "Markdown: export",
},
},
}
Start it with the first Markdown file you open. With follow_buffer (on by default), the preview then switches to whichever buffer you are in, if it can be previewed.
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
once = true,
command = "MarkdownPreview",
})
[[Page]] and [[Page|label]] links and Obsidian callouts (> [!info] Title, foldable > [!todo]-) work out of the box. Clicking a wiki link opens the note in Neovim, matching names regardless of case, spaces, - and _. Add your own look with custom_css = "~/notes/preview.css".
Check out the branch and run :MdPreviewDiff main. Every changed paragraph is marked against main, with ↑ / ↓ in the preview header to walk through them. :MdPreviewDiff staged shows what you are about to commit, and :MdPreviewDiff off hides it again.
Browser-made PDFs (the default) use the preview's styles, so custom_css with a print rule is all you need:
/* ~/.config/nvim/preview.css */
@media print {
#content { font-family: "Inter", sans-serif; }
#content h1 { color: #0b3d91; }
}
require("md_preview").setup({
custom_css = "~/.config/nvim/preview.css",
export = { paper = "a4", toc = true, numbering = true },
})
For full control over the page, use Typst and a template:
require("md_preview").setup({
export = { pdf_engine = "typst", template = "~/.config/nvim/report.typ" },
})
// ~/.config/nvim/report.typ
#let template(doc) = {
set page(header: align(right)[Internal])
set text(font: "Inter", size: 10pt)
doc
}
Then :MdPreviewExport pdf. Relative images are resolved from the document's folder.
Set host = "0.0.0.0". Each time the preview starts, Neovim shows LAN URLs with a one-time token. Open one on your tablet and it follows your cursor like the desktop browser does.
| Command | |
|---|---|
:MarkdownPreview / :MdPreview |
Start the preview |
:MarkdownPreviewStop / :MdPreviewStop |
Stop it |
:MarkdownPreviewToggle |
Start or stop |
:MdPreviewExport {format} [path] |
Export the buffer: pdf, slides (PDF, one slide per page), docx, html, epub, markdown. Saved next to the file unless path is given |
:MdPreviewDiff [base] |
Toggle the Git diff, or compare with base (HEAD, staged, a branch, tag or commit). off hides it |
:MdPreviewInfo [copy] |
Show the preview URL, the file, and which PDF and PlantUML tools are used. copy copies the URL |
:MdPreviewInstall |
Download or update the binary, pandoc and Typst |
In the preview: t table of contents, / or Ctrl+F search, p slides, click to jump. The toolbar buttons do the same. If these keys clash with a browser extension such as Vimium, set browser_keys = false (see FAQ).
Full reference: :help md_preview.
Every option is optional. Call setup() with only what you want to change:
require("md_preview").setup({
theme = "light",
git_diff_base = "main",
export = { paper = "us-letter", toc = true },
})
| Option | Default | Description |
|---|---|---|
filetypes |
{ "*" } |
File types (Neovim filetype) that can be previewed. "*" means every file. Markdown, vimwiki, Quarto, R Markdown, pandoc and MDX are rendered as documents; other files are shown as highlighted code. Special buffers (help, file explorers, terminals) are never previewed. |
max_file_size |
2097152 (2 MB) |
Files larger than this, in bytes, are not previewed. Binary files are never previewed. |
port |
9876 |
Port of the preview server. If it is taken, the next free port is used. |
host |
"127.0.0.1" |
Address to listen on. "0.0.0.0" lets other devices on your LAN open the preview; a token is then required (see "Follow along on a tablet" in Recipes). |
auto_open |
true |
Open the browser when the preview starts. |
browser_app |
false |
Open the preview in an app window without tabs or an address bar (--app of Chrome, Edge, Brave or Chromium), handy next to the editor. Uses export.browser_path, or finds one of those browsers (on WSL, the Windows one). Without one, the default browser is used. |
binary |
Downloaded copy in stdpath("data")/nvim-md-preview/ |
Path to the preview binary. Set it when you build from source. |
log_file |
"" |
File to append a log to: start and stop, the port, the tools used, export results and errors from the binary. Document contents are never written, and the LAN token is masked. Empty writes nothing. Useful for bug reports. |
| Option | Default | Description |
|---|---|---|
scroll_sync |
true |
Scroll the preview to the block under the cursor. |
cursor_highlight |
true |
Outline the block under the cursor in the preview: the paragraph, heading, list item, table or code block, or the line in code files. Works with scroll_sync = false too. Style it with .cursor-block in custom_css. |
follow_buffer |
true |
Switch the preview when you enter another buffer that can be previewed (see filetypes). Special buffers such as help, file explorers and terminals are skipped. |
debounce_ms |
80 |
Milliseconds to wait after typing before the preview updates. |
theme |
"auto" |
"auto" follows the OS, or "dark" / "light". A theme chosen with the button in the preview is remembered and takes precedence. |
custom_css |
"" |
Path to a CSS file loaded after the built-in styles, so it can override them. |
wiki_links |
true |
Render [[Page]] and [[Page|label]] as links that open in Neovim. |
task_toggle |
true |
Click a task list checkbox (- [ ]) in the preview to toggle it in the Neovim buffer, as one change you can undo. If the preview is behind the buffer (for example, right after typing), nothing is changed and you are asked to try again. Not available in LAN mode. |
browser_keys |
true |
Keyboard shortcuts in the preview (t, /, Ctrl+F, p). Set false if they clash with a browser extension such as Vimium (see FAQ). |
| Option | Default | Description |
|---|---|---|
git_diff |
true |
Show changes against git_diff_base in the preview. Toggle with :MdPreviewDiff. Files outside a Git repository show nothing. |
git_diff_base |
"HEAD" |
What to compare against: "HEAD", "staged" (the index), or a branch, tag or commit. :MdPreviewDiff <base> changes it for the session. |
git_diff_colors |
true |
Use the editor's diff colors (GitSignsAdd, Added, DiffAdd, ...). Colors that are hard to read on the preview's theme fall back to green, yellow and red. |
| Option | Default | Description |
|---|---|---|
plantuml_path |
"" |
The plantuml command or a plantuml.jar (run with java -jar) used for ```plantuml and ```puml blocks. Empty uses plantuml on PATH. Without it, the blocks are shown as code. |
plantuml_server |
"" |
A PlantUML server URL, for example "https://www.plantuml.com/plantuml". When set, the browser loads the diagrams from this server instead of running plantuml, so the diagram sources leave your machine. Empty never uses a server. |
export = { ... })| Option | Default | Description |
|---|---|---|
formats |
{ "pdf", "slides", "docx", "html", "epub", "markdown" } |
Formats offered in the preview's Export menu and in :MdPreviewExport completion. {} hides the Export button. |
pdf_engine |
"browser" |
How PDFs are made. "browser" prints the preview with Chrome, Edge, Brave or Chromium, so the PDF looks like the preview; without one of them, Typst is used. "typst" always uses Typst (supports template, but not Mermaid, math or callouts). |
browser_path |
"" |
Browser used for PDFs. Empty looks for Chrome, Edge, Brave and Chromium in their usual places and on your PATH. |
docx_engine |
"pandoc" |
"pandoc" makes :MdPreviewInstall download pandoc for DOCX, HTML and EPUB. Any other value skips it. |
pandoc_path |
"" |
Path to pandoc. Empty uses the downloaded copy, then pandoc on your PATH. |
typst_path |
"" |
Path to Typst. Empty uses the downloaded copy, then typst on your PATH. |
paper |
"a4" |
PDF paper size. Any Typst paper name, such as "us-letter" or "a5". |
margin |
"2cm" |
PDF page margin. |
font |
"" |
PDF body font. Empty uses Noto Serif CJK JP with fallbacks. |
font_size |
11 |
PDF body font size in points. |
toc |
false |
Add a table of contents (PDF, DOCX, HTML, EPUB). |
numbering |
false |
Number headings (PDF, DOCX, HTML, EPUB). |
slide_size |
"16:9" |
Page shape of :MdPreviewExport slides: "16:9" (13.33 x 7.5 in) or "4:3" (10 x 7.5 in). Slides are printed with Chrome, Edge, Brave or Chromium, whatever pdf_engine is; a slide too long for one page is scaled down to fit. |
template |
"" |
Typst file that exports a template function, applied to the whole PDF when pdf_engine = "typst" (see Recipes). |
Without setup(), these variables are read when the plugin loads:
| Variable | Same as |
|---|---|
g:md_preview_port |
port |
g:md_preview_auto_open |
auto_open (0 turns it off) |
-- Markdown preview (nvim-md-preview)
require("md_preview").setup({
-- General
filetypes = { "*" }, -- file types that can be previewed ("*" means all)
max_file_size = 2 * 1024 * 1024, -- larger files are not previewed (bytes)
binary = vim.fn.stdpath("data") .. "/nvim-md-preview/nvim-md-preview", -- path to the binary
port = 9876, -- server port (the next free one if taken)
host = "127.0.0.1", -- address to listen on ("0.0.0.0" for the LAN)
auto_open = true, -- open the browser when the preview starts
browser_app = false, -- open in an app window without tabs or an address bar
log_file = "", -- file to write a log to (empty: none)
-- Preview
scroll_sync = true, -- scroll the preview with the cursor
cursor_highlight = true, -- outline the block under the cursor
follow_buffer = true, -- switch when you enter another previewable buffer
debounce_ms = 80, -- wait after typing before updating (ms)
theme = "auto", -- "auto" (follow the OS) | "dark" | "light"
custom_css = "", -- extra CSS file
wiki_links = true, -- render [[Page]] as links that open in Neovim
task_toggle = true, -- click a checkbox to toggle it in the buffer
browser_keys = true, -- keys in the preview (t, /, Ctrl+F, p)
-- Git diff
git_diff = true, -- show changes in the preview
git_diff_base = "HEAD", -- compare with "HEAD" | "staged" | a branch, tag or commit
git_diff_colors = true, -- use the editor's diff colors
-- PlantUML
plantuml_path = "", -- plantuml command or plantuml.jar (empty: from PATH)
plantuml_server = "", -- PlantUML server URL (used only when set)
-- Export
export = {
formats = { "pdf", "slides", "docx", "html", "epub", "markdown" }, -- formats in the Export menu
pdf_engine = "browser", -- how PDFs are made: "browser" | "typst"
browser_path = "", -- browser for printing PDFs (empty: found automatically)
docx_engine = "pandoc", -- converter for DOCX, HTML and EPUB
pandoc_path = "", -- path to pandoc (empty: the downloaded one)
typst_path = "", -- path to Typst (empty: the downloaded one)
paper = "a4", -- paper size
margin = "2cm", -- page margin
font = "", -- body font (empty: default)
font_size = 11, -- body font size (pt)
toc = false, -- add a table of contents
numbering = false, -- number headings
slide_size = "16:9", -- page shape of the slides PDF: "16:9" | "4:3"
template = "", -- Typst template (with pdf_engine = "typst")
},
})
nvim-md-preview uses the same command names as markdown-preview.nvim: :MarkdownPreview, :MarkdownPreviewStop and :MarkdownPreviewToggle. The table shows the equivalent of each markdown-preview.nvim setting:
| markdown-preview.nvim | nvim-md-preview |
|---|---|
build = "cd app && npx --yes yarn install" |
build = ":MdPreviewInstall" |
g:mkdp_port |
port |
g:mkdp_theme |
theme |
g:mkdp_markdown_css |
custom_css |
g:mkdp_open_to_the_world = 1 |
host = "0.0.0.0" (protected by a token) |
g:mkdp_preview_options.disable_sync_scroll = 1 |
scroll_sync = false |
g:mkdp_auto_start = 1 |
The auto-start recipe |
g:mkdp_browser |
Not supported yet. Set auto_open = false and open the URL yourself |
Browser extensions that add Vim-style keys, such as Vimium, Vimium C, Tridactyl and Surfingkeys, use the same keys as the preview:
| Key | Preview | Vim-style extensions |
|---|---|---|
t |
Table of contents | Open a new tab (Vimium, Tridactyl) |
/ |
Search | Find in page (Vimium), quick find (Firefox) |
p |
Presentation mode | Open the clipboard URL (Vimium, Tridactyl) |
f (in slides) |
Fullscreen | Link hints |
The extension usually receives the key first, so the preview never sees it. You can do either of these:
Turn off the preview's shortcuts and use the toolbar buttons instead:
require("md_preview").setup({ browser_keys = false })
Turn off the extension on the preview page so the preview's keys work:
http://127.0.0.1:*/*. To keep the extension and only pass some keys through, put them in "Keys", for example t / p.:blacklistadd 127.0.0.1.settings.blocklistPattern = /127\.0\.0\.1/; to your settings.If you changed port or use LAN mode, adjust the address accordingly.
Each Neovim runs its own preview server. WSL and Windows have separate localhost networks, but the browser runs on Windows, so in WSL the server skips ports that are in use on Windows (checked with netstat.exe; see :checkhealth md_preview). If a URL still reaches a preview started by another Neovim, the page says "Another preview is using this port" instead of showing the other document. Set a different port in that case.
No. The server listens on 127.0.0.1 only, and everything it needs is inside the binary. See Security. The only exception is plantuml_server: if you set it, PlantUML diagram sources are sent to that server.
Neovim ──msgpack-rpc (stdin/stdout)──▶ Rust binary ──WebSocket──▶ Browser
▲ │ pulldown-cmark → HTML │
└──────────── click / open link ──────┴──────────────────────────────────┘
The Lua side sends the buffer (debounced) and the cursor line. The binary converts Markdown to HTML with source-line markers, serves the page and assets on 127.0.0.1, and pushes updates over a WebSocket. Clicks in the preview travel back the same way.
The server binds to 127.0.0.1 unless you set host. It rejects requests whose Host is not a loopback name (DNS rebinding) and any cross-origin request or WebSocket, so other websites open in your browser cannot read your files or control Neovim. /assets only serves files inside the document's directory. In LAN mode every request needs the session token.
Versions up to v0.1.0 allowed other websites to read local files while a preview was running. Update with :MdPreviewInstall.
Bug reports, ideas and pull requests are welcome. See CONTRIBUTING.md for building from source, tests and the release flow.
git clone https://github.com/ywsrock/nvim-md-preview
cd nvim-md-preview
bash download-libs.sh && cargo build --release
MIT