Open-source project
vercel-labs/wterm avatar
vercel-labs/wterm

wterm: a browser terminal that repaints only the rows it touched

A terminal emulator for the web

3,680 stars176 forksTypeScriptApache-2.0

At a glance

What is it?
A web terminal emulator whose escape sequence parser is written in Zig and compiled to WebAssembly, rendering into the DOM so that selection, clipboard and screen readers work on real text. The feature list reads like a list of everything that goes wrong when a terminal lives in a browser.
Who is it for?
Use wterm if you are putting a real shell in a web page and you want selection, clipboard and assistive technology to behave rather than be reimplemented, since the DOM rendering choice is what buys all three. It ships framework bindings for the common component libraries and an in-browser shell for demos, and the Ghostty backend is there when VT compliance matters more than idle memory.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • vercel-labs/wterm3,680 ★ · +211
Star history
Repository2026-09-242026-11-13
vercel-labs/wterm3.5k3.7k

211 Stars in the last 50 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

It renders to the DOM, and that is the whole bet

Most terminal emulators for the browser draw to a canvas, which means they own selection, the clipboard, find in page, and whatever assistive technology you are able to fake.

This one renders into the DOM instead, and the feature list is explicit about what that buys: native text selection, clipboard handling, the browser's own find, and screen reader support, all operating on the mounted rows.

The escape sequence parser is not JavaScript. The core is written in Zig and compiled to WebAssembly, described as near-native performance, and the release build of the compiled binary is about twenty six kilobytes for the VT100, VT220 and xterm sequence parser.

Twenty six kilobytes is the number that makes the design coherent. A full terminal emulator in a browser is usually measured in hundreds of kilobytes before you render a character. A parser that small, with the rendering done by the browser's own text engine, is a different class of artefact: it is small enough to load before first paint and to share between terminals.

So the choice is deliberate at every layer: the browser does text, the core does sequences.

Two cores, and the bigger one trades idle memory for compliance

There are two emulator cores and you choose one.

The built-in core is the small Zig parser described above. The alternative is a backend powered by an existing terminal implementation, offered as opt in, and the reason to want it is full VT compliance rather than partial support.

The cost is stated rather than hidden. That backend allocates terminal pages as needed, which reduces the memory reserved by terminals that are sitting idle and by alternate screens. So the compliant core is cheaper when you are using the terminal, and dearer when you are not, because it reserves less only by growing on demand.

Then there is the loading model, which is the part most write-ups would skip. Both cores reuse the same compiled modules while keeping their terminal memory separate. The built-in binary loads only when you select it, and is decoded once no matter how many terminals you open. And browsers that need to compile the module separately do that during download rather than at first use.

That last clause matters for perceived startup. Compiling WebAssembly on the main thread during the first keystroke is how browser terminals feel slow, so paying it during the download is a small decision with a large effect.

Eight packages: a bridge, four renderers, a shell, a mode

The package table is the clearest description of the architecture, so it is worth taking at face value.

One headless package is the bridge: the WebAssembly core behind an interface, plus a WebSocket transport. Everything else talks to that interface rather than to the browser.

Then four rendering packages. One is a plain DOM renderer with an input handler for vanilla JavaScript. Then one component each for React with a hook, Vue 3 with a template ref, and Svelte with callbacks. Those three differ in their integration style rather than in capability, which is the right call: each binding looks native to its framework instead of forcing the same shape on three different ecosystems.

Then the two optional cores, one being the full emulation backend and one being an in-browser shell powered by a JavaScript shell implementation, plus a package that renders Markdown in the terminal.

So there are two cores, four rendering paths, a shell that needs no server, and a Markdown mode. That is a bigger surface than a demo requires, and it is the shape you would expect from a project that intends to be both a product and a set of building blocks.

Only touched rows repaint, and writes skip the timer

The rendering performance claims are specific enough to be checked, and they come down to two ideas.

The first is dirty-row tracking. On each animation frame, only rows that were touched get re-rendered. Partial redraws reuse the cell spans that did not change, and a text change reuses the existing text node when the styling and the column widths still match.

That last clause is the one that matters and the one most implementations get wrong. Reusing a text node is only safe if nothing about its box changed, so the condition is stated explicitly rather than assumed.

The second is frame-direct scheduling. A write queues its render on the next animation frame directly, without an extra timer hop in between. A timer between the write and the paint means an extra scheduling delay per frame, which at sixty frames a second is the difference between smooth and visibly behind.

Two related features sit next to them. A synchronized output mode blocks painting atomically, with a bounded deadline for recovery if a program never signals completion. And the built-in core answers terminal queries: operating status reports, cursor position, and the primary device attributes for the VT100 features it supports, so applications that ask get an answer instead of a timeout.

Selection has six separate claims, and each one is a known failure

Half the feature list is about selecting text, which tells you exactly where browser terminals break.

Copy is terminal aware: selected text preserves explicit newlines and complete Unicode cells, includes block glyphs, and joins soft wraps once they are confirmed. That last part is a subtle one, because a soft-wrapped line looks like two lines to the browser and like one line to the user.

Select all reaches the retained history and the active screen without mounting extra rows, which is the difference between instant and a visible stall on a long session.

Word and line selection: double click picks paths and words, triple click picks logical lines, and both work across confirmed soft wraps and in history that is not currently mounted.

Rectangular selection copies columns with a modifier drag, preserving the selected spaces, which is what you need for logs and tables where column alignment carries meaning.

Selection survives scrolling and reflow in the compliant core, with the selected history mounted separately from the viewport, so the selection does not vanish when the terminal repaints.

And hyperlinks stay attached to their exact cells, through viewport and scrollback, rendered as safe HTTP anchors. A link that follows the text as it scrolls is the difference between a working terminal and a terminal you cannot click anything in.

The local workspace is a shell client with flow control and reconnection

The features prefixed with local are about running real shells on a real machine and surviving the network.

Sending is bounded: byte and message limits, ordered draining, and observable backpressure when a connection is slow or gone. Bounded means a slow consumer cannot make the server buffer without limit, and observable means the application can see it happening rather than discovering it as a hang.

The local side batches small output messages together, pauses fast output until the browser catches up, preserves bytes across frames, and reports rejected input rather than discarding it silently.

Reconnection is the feature to read twice. A brief interruption resumes the same shell and the same terminal state in the existing page, with bounded output replay, acknowledgements on input delivery, duplicate suppression, and explicitly no automatic input retries. Every clause is there to answer the question of what happens to the keystroke you pressed during the outage.

Reload recovery restores split layouts, pane sizes, zoom and which terminal was focused, alongside saved shells, within documented storage and session limits.

And when a pane is not visible, the renderer skips its paints, for hidden sessions and for hidden browser documents, while output and terminal replies continue to accumulate. Background tabs are where a naive renderer burns a core.

Six named benchmarks and a preflight that refuses to start

The tooling is the last section worth reading, because a terminal project that does not measure anything is guessing.

There are six benchmark scripts at the root, and they are named after what they measure: the terminal itself, input, the path from a pseudo-terminal through input, search, stability, and overall performance. Each one builds an internal pseudo-terminal harness first and then runs a bench target inside it.

The fact that pseudo-terminal input and search are measured separately from raw throughput is the interesting part. Those are the two things a browser terminal is worst at and the two a user notices first.

One of them looks like this:

code
turbo run build --filter='@internal/pty-harness^...' && pnpm --filter @internal/pty-harness bench

End to end tests run through Playwright with a dedicated configuration, unit tests through a workspace-aware runner, and everything fans out through a task runner.

Then there is a preflight script that refuses to continue. The development command has a preceding step that checks for an external command line tool on the path, and if it is missing it prints what to install and where, and exits. A build that tells you its missing prerequisite and stops is better than one that fails three minutes later with a module error.

The toolchain requirements are stated in the manifest: a recent major version of the JavaScript runtime and a recent major version of the package manager, with the runtime version also pinned in a file at the root. There is a script to synchronise versions across the eight packages. And three releases landed within about a day of each other, which is the pace of a zero point five.

Editorial conclusion

Use wterm if you are putting a real shell in a web page and you want selection, clipboard and assistive technology to behave rather than be reimplemented, since the DOM rendering choice is what buys all three. It ships framework bindings for the common component libraries and an in-browser shell for demos, and the Ghostty backend is there when VT compliance matters more than idle memory. Do not adopt it on the assumption it is stable: it is a Vercel Labs experiment shipping version zero point five, and its own quick start refuses to run without an external tool installed. Before you rely on it, read the connection handling features, because the reconnect and flow control behaviour is where a browser terminal either recovers or silently drops your input, and check the benchmark targets, since the project names six and that is a better signal than any claim about speed.

Frequently asked questions

What is wterm?

A terminal emulator for the web that renders into the DOM, so native text selection, clipboard handling, the browser's find and screen readers all work on the mounted rows. Its escape sequence parser is written in Zig and compiled to WebAssembly, and the release build of that binary is about 26 KB.

What is the difference between the wterm cores?

There is a lightweight built-in Zig core and an opt in backend powered by an existing terminal implementation for full VT compliance. The larger one allocates terminal pages as needed, which lowers the memory reserved by idle terminals and alternate screens.

Which frameworks does wterm support?

A headless core package plus a DOM renderer for vanilla JavaScript, and components for React with a hook, Vue 3 with a template ref, and Svelte with callbacks. The manifest requires a recent major version of the JavaScript runtime and of the package manager.

How does wterm handle slow or dropped connections?

Sends over the socket are bounded by byte and message limits with ordered draining and observable backpressure. The local workspace batches small output and pauses fast output until the browser catches up, and a brief interruption resumes the same shell with bounded output replay, input acknowledgements and duplicate suppression.

Can I run a shell in wterm without a server?

Yes, one package provides an in-browser shell powered by a JavaScript shell implementation, and another renders Markdown in the terminal. The examples directory includes local, ssh and markdown-streaming cases alongside framework examples.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. vercel-labs/wterm on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vercel-labs-wterm.svg)](https://hysenlabs.com/projects/vercel-labs-wterm)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/vercel-labs-wterm.svg)](https://hysenlabs.com/projects/vercel-labs-wterm)

Was this useful?