Introduction
Neovim separates its UI from its editing engine and can be controlled by an external UI through Msgpack-RPC. This suggests another possibility: compile Neovim itself to WebAssembly and run it inside the browser, instead of connecting a web page to Neovim on a server.
I created nvim-wasm to build Neovim for the WebAssembly System Interface (WASI) and operate it directly in a browser.
Neovim maintainer Justin M. Keyes shared the nvim-wasm repository and demo on X!
https://x.com/justinmk/status/1999312671817322729
The repository is available here:
A minimal demo is available here:
The minimal demo starts Neovim in the browser and allows normal mode changes and text input.
This article explains what was required to port Neovim to Wasm and how the browser communicates with it.
Overview
nvim-wasm contains a build wrapper that cross-compiles Neovim for wasm32-wasi and several demos that operate the resulting module in a browser.
The build produces this WASI module:
build-wasm/bin/nvim
It is not a host executable. In a browser, it is loaded by a WASI runtime inside a Web Worker and started with a virtual filesystem and connected standard I/O.
The repository contains four demos with different renderers and input transports.
| Demo | Renderer | Input transport | Live demo |
|---|---|---|---|
examples/demo | DOM linegrid | SharedArrayBuffer | Open |
examples/demo-asyncify | DOM linegrid | postMessage and Asyncify | Open |
examples/demo-monaco | Neovim buffer and cursor mirrored into Monaco Editor | SharedArrayBuffer | Open |
examples/demo-xterm | linegrid converted to ANSI and rendered by ghostty-web | SharedArrayBuffer | Open |
These are not browser editors that imitate Vim. A real Neovim process runs inside the Worker, receives key input, and returns the state that is rendered on the page.
How it works
Cross-compiling for WASI
The build uses WASI SDK. A CMake toolchain file sets the target to wasm32-wasi and selects the WASI SDK versions of Clang, the sysroot, ar, and ranlib.
Lua, libuv, luv, lpeg, Tree-sitter, utf8proc, and unibilium are also built as static WASI libraries before being linked into Neovim.
WASI SDK, CMake, and Binaryen are downloaded under .toolchains, so they do not need to be installed system-wide.
Running code generation with host Lua
Neovim’s build executes Lua to generate source files. A cross-compiled Wasm Lua executable cannot run directly on the Linux machine performing the build.
The build therefore creates a native Lua interpreter and nlua0 first.
make host-lua
When source generation is required during the Wasm build, scripts/build/host_lua_gen.lua runs it with the native Lua interpreter. Separating programs that will become Wasm from programs that must execute during the build makes the cross-compilation possible.
Providing functionality missing from WASI
Neovim and libuv normally use Unix facilities such as PTYs, signals, pthreads, and ioctl. The WASI environment does not expose all of these facilities in the same form.
nvim-wasm provides WASI-specific headers and stubs under patches/wasi-shim. It also patches libuv and luv, separating operations that can work under WASI from unsupported operations that return ENOSYS.
The Neovim submodule itself remains unchanged. CMake overrides and external patches replace sources and add compile options. This keeps the WASI-specific differences visible when updating the referenced Neovim revision.
Starting Neovim in a Web Worker
Neovim runs inside a Web Worker. The main thread handles browser input and rendering, while the Worker executes Neovim.
Because a browser has no normal host filesystem, the Neovim runtime, the WASI-built libraries, and version data are distributed in a tar.gz archive. The Worker extracts this archive into its virtual filesystem at startup.
tar -czf examples/demo/nvim-runtime.tar.gz \
-C neovim/.. runtime \
-C build-wasm usr nvim_version.lua
Connecting through Msgpack-RPC
Neovim starts with --embed and uses standard I/O as its Msgpack-RPC transport.
The main thread calls nvim_ui_attach and receives ext_linegrid redraw notifications. These notifications contain characters, highlights, cursor positions, modes, and other UI state. The minimal demo renders them directly into a DOM grid.
Keyboard events are sent to Neovim through nvim_input. Neovim itself handles the input, mode transitions, and buffer edits.
Browser keyboard input
└─ nvim_input
└─ Neovim in a Web Worker
└─ redraw notifications
└─ DOM renderer
Using SharedArrayBuffer
The standard demos use a SharedArrayBuffer ring buffer for stdin. The main thread writes input into the ring, and Neovim in the Worker reads it.
Using SharedArrayBuffer in a browser requires a cross-origin-isolated page. The server must return these COOP and COEP headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Avoiding SharedArrayBuffer with Asyncify
I also added a demo for static hosting environments where COOP and COEP cannot be configured.
During ordinary Wasm execution, the Worker cannot receive a postMessage while Neovim is blocked waiting for input. The completed Wasm module is therefore post-processed with Binaryen Asyncify so execution can be suspended at WASI’s poll_oneoff.
make wasm-asyncify
The Wasm state is saved while waiting for input and restored after the Worker receives a message. This removes the COOP/COEP requirement, at the cost of a larger module and additional runtime overhead.
The artifacts currently included in the repository are approximately 7.8 MB for the standard build and 8.4 MB for the Asyncify build.
The live Asyncify demo is available here:
Examples
Monaco Editor
The Monaco demo does not delegate editing to Monaco. Monaco remains read-only and every key event is forwarded to Neovim.
The browser subscribes to buffer changes using nvim_buf_attach, then mirrors Neovim’s buffer, cursor, and visual selection into Monaco. Monaco provides the visual surface, while Neovim owns the editing state.
The live Monaco demo is available here:
Terminal UI
The terminal demo converts ext_linegrid updates into ANSI escape sequences and renders them through ghostty-web, which exposes an xterm.js-compatible API.
This does not run Neovim under a PTY and stream a terminal screen. It converts Neovim’s external UI protocol into terminal output.
The live terminal demo is available here:
Building it
Install a Lua 5.1-compatible interpreter, curl, and tar. Clone the repository recursively because Neovim is included as a submodule.
git clone --recursive https://github.com/MuNeNiCK/nvim-wasm.git
cd nvim-wasm
Build the native Lua tools, the Wasm dependencies, and Neovim in that order.
make host-lua
make wasm-deps
make wasm
The build creates the following WASI module:
build-wasm/bin/nvim
To run the minimal demo, package the runtime and copy the module.
tar -czf examples/demo/nvim-runtime.tar.gz \
-C neovim/.. runtime \
-C build-wasm usr nvim_version.lua
cp build-wasm/bin/nvim examples/demo/nvim.wasm
python3 examples/demo/serve.py
Open the following URL:
http://localhost:8765/
serve.py adds the COOP and COEP headers required by SharedArrayBuffer. Generate the Asyncify build with make demo-asyncify and serve it from examples/demo-asyncify/serve.py.
Limitations
WASI does not expose every facility available on a desktop operating system. The current nvim-wasm implementation has particular limitations around:
- PTYs and child processes
- operations that depend on signals or pthreads
- host-like networking, sockets, and filesystem watching
- plugins that require native shared libraries
- direct access to files outside the browser sandbox
At this stage, nvim-wasm is not a drop-in replacement for a normal Neovim installation. It brings Neovim’s editing engine, Lua, Msgpack-RPC, and external UI into the browser.
Future work
Upstream Neovim Wasm support
Work is underway to make Wasm an officially supported Neovim build target.
The Wasm tracking issue, opened in August 2025, proposes a browser-capable Neovim artifact, documented build steps, and CI coverage. It lists nvim-wasm as one of its reference implementations.
The GSoC 2026 tracking issue covers an official Emscripten build target, browser RPC, SharedArrayBuffer-based standard I/O, and a browser UI. nvim-wasm uses WASI SDK, while the current upstream effort is based on Emscripten.
As part of the GSoC 2026 work, PR #40008, which added Emscripten build support, was merged in June 2026. As of July 30, 2026, PR #40772, which adds browser support for Neovim, remains open.
In the past, Saroj, who was considering participating in GSoC, asked whether nvim-wasm could be used as a reference. I gave my permission. The same applies going forward: Neovim pull requests and GSoC projects are welcome to use the design and implementation of nvim-wasm as a reference.
If you refer to it in an upstream Neovim pull request or a GSoC project, I would appreciate it if you also listed MuNeNiCK under Contributors. If there is no Contributors section, an equivalent mention under Acknowledgements is also welcome.
Applications in vscode-neovim and Firenvim
I expect nvim-wasm to be useful beyond its demo pages, particularly for existing tools that use Neovim as an editing backend.
For example, vscode-neovim integrates Neovim with VS Code’s editing features. It currently requires the user to install Neovim separately, after which the extension starts that Neovim binary as its backend.
There is already a feature request to bundle Neovim with vscode-neovim as Wasm. A Wasm build could allow users to install only the extension, without separately installing Neovim or preparing an environment in which to run its backend. It could also keep the extension and Neovim versions aligned without distributing a different native binary for every operating system.
Firenvim is a browser extension that allows elements such as web page textareas to be edited with Neovim. It currently installs Firenvim as a local Neovim plugin and starts Neovim through the browser’s Native Messaging mechanism.
Running Neovim as Wasm inside the browser could make a similar integration possible without a local Neovim installation or Native Messaging host. The same idea could apply beyond a dedicated Firenvim-style UI, allowing ordinary text input on web pages to be processed through Neovim.
Practical implementations still need to address filesystem access, configuration, plugins, and browser permission constraints. If the idea interests you, why not use nvim-wasm as a reference and try implementing it?
Conclusion
Running Neovim in a browser required more than compiling its C code to Wasm. The build-time code generators, differences between libuv and WASI, virtual filesystem, blocking input, Msgpack-RPC transport, and UI renderer all had to be connected.
nvim-wasm provides several ways to explore those connections: a minimal DOM grid, Monaco Editor, a terminal renderer, and an Asyncify-based transport.
I hope the implementation and the lessons learned from it will also be useful as upstream Neovim’s Wasm support progresses.
References
- MuNeNiCK/nvim-wasm
- Neovim: tracking: WASM as Nvim build target
- Neovim: GSoC 2026 WASM/Emscripten tracking issue
- Neovim PR #40008: add wasm/emscripten build support
- Neovim PR #40772: add browser support for Neovim
- vscode-neovim
- vscode-neovim: ship Nvim built as WebAssembly
- Firenvim
- WASI SDK
- Binaryen Asyncify
- Neovim UI events