CLI tool
coder/ghostty-web avatar
coder/ghostty-web

ghostty-web: Ghostty's VT100 parser in the browser, behind an xterm.js API

Ghostty for the web with xterm.js API compatibility

2,922 stars177 forksTypeScriptMIT

At a glance

What is it?
ghostty-web wraps a WASM build of Ghostty's terminal emulator in an xterm.js-compatible API. The migration is one import line, but the bundle is a 400KB WebAssembly file and the escape-sequence coverage is the actual selling point.
Who is it for?
Adopt ghostty-web if you already ship an xterm.js-based terminal and your users hit grapheme-handling or XTPUSHSGR/XTPOPSGR gaps that xterm.js documents as unsupported. Do not adopt it if you need a rendering-only widget with no WASM payload, or if you cannot serve a .wasm file alongside your JavaScript bundle.
Can I use it commercially?
Yes. MIT 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 last received commits 90 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

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

Editorial analysis

What ghostty-web replaces, and who feels the difference

The project targets one job: render a terminal inside a browser tab using Ghostty's emulator instead of a reimplementation. The README frames the problem as xterm.js reimplementing terminal emulation in JavaScript, with "every escape sequence, every edge case, every Unicode quirk" hand-coded. ghostty-web takes the parser that ships in the native Ghostty app, compiles it to WebAssembly, and exposes it through an API shaped like xterm.js.

The audience is narrow and specific. If you maintain a web terminal, a cloud IDE, or an agent dashboard that already embeds xterm.js, you are the intended user, and the README states the migration is an import change from @xterm/xterm to ghostty-web. If you are building a terminal from scratch, the xterm.js compatibility surface is less valuable to you, but the parser is still the reason to look. The project was originally created for Mux, a Coder desktop app for isolated, parallel agentic development, and the README says it is designed to be used anywhere.

A WASM parser behind an xterm.js-shaped API

Two layers make up the library. The lower layer is a WebAssembly module named ghostty-vt.wasm, built from Ghostty's source with a patch in patches/ghostty-wasm-api.patch that exposes additional functionality. The package.json exports map exposes that file under the subpath ./ghostty-vt.wasm, and the build script build:wasm-copy copies it into dist/, so the binary sits next to the JavaScript in the published package.

The upper layer is TypeScript. The README's usage example imports init and Terminal, awaits init() before constructing a Terminal, then wires term.onData to a WebSocket send and term.write to incoming messages. That is the same shape an xterm.js integration already has, which is what makes the import swap plausible rather than aspirational.

The README claims zero runtime dependencies and roughly 400KB for the WASM bundle. Treat the size as the honest cost of the design: you are shipping a compiled emulator, not a thin renderer. The comparison table lists two concrete advantages over xterm.js, proper grapheme handling for complex scripts such as Devanagari and Arabic, and full XTPUSHSGR/XTPOPSGR support, where the table links an xterm.js issue marking it unsupported. Those are the claims worth testing against your own traffic.

Installing ghostty-web and opening a first terminal

Install from npm. The package ships dist/ and ghostty-vt.wasm, per the files field in package.json, so a normal bundler setup picks up both the JavaScript entry points and the WASM asset.

bash
npm install ghostty-web

Then initialize the WASM module once before creating a terminal. The README's example awaits init() at module scope, which means the parser is ready before any escape sequences arrive.

javascript
import { init, Terminal } from 'ghostty-web';

await init();

const term = new Terminal({
  fontSize: 14,
  theme: {
    background: '#1a1b26',
    foreground: '#a9b1d6',
  },
});

Attach it to a DOM element and connect the data flow. term.open takes the container, onData carries keystrokes outward, and write pushes server output into the emulator.

javascript
term.open(document.getElementById('terminal'));
term.onData((data) => websocket.send(data));
websocket.onmessage = (e) => term.write(e.data);

The README points to demo/index.html for a full client-to-server example. If you would rather see it running before writing any code, the demo package starts a loopback-only HTTP server with a real shell on http://127.0.0.1:8080.

bash
npx @ghostty-web/demo@next

The README notes the demo protects /ws with a per-run same-origin token and rejects cross-origin WebSocket handshakes. It works best on Linux and macOS. Binding elsewhere requires HOST, and serving through extra hostnames or a wildcard bind such as HOST=0.0.0.0 also requires GHOSTTY_ALLOWED_HOSTS. The README warns against remote exposure because the demo starts a real local shell.

The WASM bundle is a deployment constraint, not a footnote

The most concrete limitation is the artifact itself. A roughly 400KB WebAssembly file must be fetched and instantiated before the terminal can parse anything, and init() is asynchronous. Any integration that assumes a synchronous constructor and immediate first paint has to be restructured around that await. The README does not document a fallback path when the WASM fetch fails, so a failed instantiation is an application-level problem you own.

Building the library from source is a heavier commitment than consuming it. The README states the build requires Zig and Bun, and the build script chains build:wasm, build:lib and build:wasm-copy. That is a toolchain most JavaScript teams do not have installed, and it exists because the WASM comes from Ghostty's source rather than from a published binary distribution. The README says the project will eventually consume a native Ghostty WASM distribution once available, which tells you the current build path is a bridge, not a destination.

There is also a scope question. ghostty-web is a terminal emulator, not a terminal application. It does not provide a PTY, a shell, or a transport. The demo's HTTP server and WebSocket token exist to make the sample safe, and the README's warning about remote exposure is a reminder that the security of the surrounding shell is entirely outside this library.

How ghostty-web differs from xterm.js in practice

xterm.js is the obvious alternative and the project treats it as such, down to the import-level compatibility. The difference is where the emulation logic lives. xterm.js implements terminal emulation in JavaScript, maintained by its own contributors, and the README's table points to at least one capability it does not support, XTPUSHSGR/XTPOPSGR, linking the open issue. ghostty-web instead compiles the emulator from Ghostty's repository and applies a patch to expose the API surface it needs.

That choice buys consistency with the native Ghostty app and inherits fixes made upstream, but it also couples the library's release cadence to Ghostty's source and to the patch staying small. The README explicitly expects the patches to get smaller over time as libghostty matures, and states the library will eventually consume a native Ghostty WASM distribution. Anyone adopting today is adopting the patched-source arrangement, not the future one.

The practical test is your own sequence corpus. If your application renders Arabic or Devanagari text in the terminal, or relies on push/pop of graphic rendition state, the README's two named gaps are the reason to switch. If your terminal only ever prints ASCII with SGR colors, the WASM payload is cost without a corresponding benefit, and staying on xterm.js keeps your bundle smaller.

Releases, maintenance and the cost of upgrading

The last push to the repository was on 2026-07-02. The most recent release is v0.4.0, published on 2025-12-09, and the repository uses release-please, with a manifest at .release-please-manifest.json and a release-please script in package.json. That setup suggests version bumps are automated from conventional commits, which lowers the cost of tracking changes but does not tell you how often they land.

The upgrade surface is the xterm.js-compatible API plus the WASM binary. Because the two ship together in the same package, a version bump can change both the JavaScript API and the parser behaviour at once. The package.json exports map pins the WASM under ./ghostty-vt.wasm, so a bundler configuration that resolves that subpath will follow the package version rather than a separately versioned binary. There is no documented compatibility matrix between library versions and Ghostty source revisions in the README, so a regression in sequence handling after an upgrade has to be bisected by version rather than checked against a table.

The licence is MIT, declared in package.json and included as LICENSE. MIT permits commercial and closed-source use and requires preserving the copyright notice and licence text. The WASM is compiled from Ghostty's source, and the README does not state Ghostty's licence terms in this document, so if you redistribute the binary in a product with licence-compliance requirements, confirm the upstream licence yourself rather than inferring it from this package's MIT declaration.

Editorial conclusion

Adopt ghostty-web if you already ship an xterm.js-based terminal and your users hit grapheme-handling or XTPUSHSGR/XTPOPSGR gaps that xterm.js documents as unsupported. Do not adopt it if you need a rendering-only widget with no WASM payload, or if you cannot serve a .wasm file alongside your JavaScript bundle. Before committing, load the demo with npx @ghostty-web/demo@next and run your own escape-sequence corpus through it, because the README's comparison table names two specific xterm.js gaps and says nothing about the long tail of sequences your application may rely on.

Frequently asked questions

Is ghostty-web free to use?

Yes. The package is published under the MIT licence, declared in package.json and included as LICENSE in the repository, which permits commercial and closed-source use provided the copyright notice and licence text are preserved.

Is ghostty-web safe?

The library itself is a browser-side terminal emulator, but the README warns that the demo starts a real local shell and advises against remote exposure unless you understand the risk. It also notes the demo protects /ws with a per-run same-origin token and rejects cross-origin WebSocket handshakes.

What is special about ghostty-web compared with xterm.js?

It compiles Ghostty's VT100 parser to WebAssembly and keeps the xterm.js API, so migration is an import change from @xterm/xterm to ghostty-web. The README's comparison table claims proper grapheme handling for complex scripts and full XTPUSHSGR/XTPOPSGR support, which it lists as unsupported in xterm.js.

How do I use ghostty-web?

Install it with npm install ghostty-web, await init() from the package, construct a Terminal with options such as fontSize and theme, call term.open on a DOM element, and connect term.onData and term.write to your WebSocket. The README points to demo/index.html for a full client-to-server example.

Official sources

  1. coder/ghostty-web on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
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/coder-ghostty-web.svg)](https://hysenlabs.com/projects/coder-ghostty-web)