W-Term: A DOM-Based Web Terminal from Vercel with Ghostty Core Support
W-Term is a web terminal emulator from Vercel that renders to the DOM rather than to an HTML <canvas>. Each terminal row becomes a div.term-row with actual text content, which means native browser text selection, copy-paste, Ctrl+F search, and screen reader parsing all work without any additional implementation.
The default rendering engine is a Zig-compiled WebAssembly binary of approximately 12KB. For applications requiring more complete VT-100 emulation, an optional Ghostty core (~400KB) is available as a drop-in replacement.
Canvas-based terminals and why DOM rendering matters
Xterm.js, the dominant web terminal library, renders to a canvas element. The browser treats the canvas as a pixel bitmap with no semantic understanding of the text drawn inside it. This means standard browser features do not work natively: text selection, copy, find, and screen reader support all require re-implementation in JavaScript. The results are functional but not equivalent to native browser behavior.
W-Term's DOM approach solves this by keeping the terminal content as actual text nodes. The trade-off is that naive DOM manipulation can be slow. W-Term avoids this with dirty-row tracking: it uses requestAnimationFrame and only re-renders the specific rows that changed since the last frame, keeping performance competitive even for applications like htop that update frequently.
Full-stack setup
Building a functional web terminal requires a frontend (W-Term) and a backend that runs a real shell process and connects to the frontend via WebSocket.
Frontend: React with W-Term
Create a Vite React project:
Install W-Term packages:
@wterm/core: framework-agnostic logic including the WebSocket transport@wterm/dom: the DOM rendering engine@wterm/react: the<Terminal />React component
Basic component:
At this point, the terminal renders but is not connected to a shell. Typing produces no output.
Backend: Node.js PTY server
The backend spawns a shell process using node-pty, which creates a pseudoterminal (PTY) master-slave pair. Node.js writes to the master end (appearing as stdin to the shell) and reads from the master end (capturing stdout and stderr). A WebSocket server bridges this to the browser.
The resize handling uses a custom escape sequence (\x1b[RESIZE:cols;rows]) to distinguish resize events from regular input in the same WebSocket channel.
Connecting the frontend to the backend
Start the backend:
Start the frontend:
The terminal now connects to the shell. Typing ls, htop, or any other shell command works as expected.
Ghostty core
The default ~12KB Zig core handles standard terminal output well. For complex TUI applications, full Unicode grapheme cluster support, or advanced terminal attributes, the Ghostty core provides more complete VT-100 emulation at the cost of a larger bundle (~400KB).
Switching to Ghostty
Add a script to copy the WASM file to the public directory before building:
Load the core asynchronously and pass it to the Terminal component:
The copy-wasm script runs before Vite, placing the WASM file in public/ where Vite's dev server can serve it. The wasmPath prop tells Ghostty where to fetch it at runtime.
Final thoughts
W-Term's DOM-based rendering is a meaningful architectural improvement over canvas-based terminals for most web applications. Native text selection, find, and accessibility work without custom implementations. The dirty-row tracking keeps performance practical even for high-frequency output.
The choice between the Zig default core and the Ghostty core is a bundle size versus emulation completeness tradeoff. For most use cases, the 12KB default is sufficient. For cloud IDEs or developer tools where users will run complex TUI applications, the Ghostty core is the better choice.
The WebSocket + node-pty server pattern shown here is straightforward to extend: add authentication, session persistence, container-scoped shells, or multiplexing by building on top of the same bidirectional channel.
Source code and documentation are at github.com/vercel/w-term.