| name | wterm-web-terminal |
| description | Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings |
| triggers | ["add a terminal to my web app","embed a terminal emulator in the browser","use wterm in my React app","connect a WebSocket PTY terminal","render a terminal in the DOM","build a web-based shell interface","integrate wterm terminal component","display terminal output in a Next.js app"] |
wterm Web Terminal Emulator
Skill by ara.so — Daily 2026 Skills collection.
wterm ("dub-term") is a web terminal emulator with a Zig/WASM core (~12 KB binary) for near-native VT100/VT220/xterm parsing. It renders to the DOM — giving you native text selection, copy/paste, browser find, and accessibility for free. Supports WebSocket PTY backends, alternate screen buffers, 24-bit color, scrollback, and themes.
Packages
| Package | Purpose |
|---|
@wterm/core | Headless WASM bridge + WebSocket transport |
@wterm/dom | DOM renderer + input handler (vanilla JS) |
@wterm/react | React component + useTerminal hook |
@wterm/just-bash | In-browser Bash shell |
@wterm/markdown | Render Markdown in the terminal |
Installation
npm install @wterm/react @wterm/core
npm install @wterm/dom @wterm/core
npm install @wterm/just-bash @wterm/core
Copy the WASM binary to your public directory:
cp node_modules/@wterm/core/wterm.wasm public/
React Usage
Basic Terminal Component
import { Terminal } from '@wterm/react';
export default function App() {
return (
<div style={{ width: '800px', height: '500px' }}>
<Terminal
wsUrl={`ws://${window.location.host}/pty`}
theme="default"
/>
</div>
);
}
useTerminal Hook
import { useTerminal } from '@wterm/react';
import { useEffect, useRef } from 'react';
export default function CustomTerminal() {
const containerRef = useRef<HTMLDivElement>(null);
const { terminal, connect, disconnect, write, resize } = useTerminal({
wsUrl: process.env.NEXT_PUBLIC_PTY_WS_URL,
wasmUrl: '/wterm.wasm',
theme: 'monokai',
scrollback: 1000,
onData: (data) => console.log('Terminal output:', data),
onConnect: () => console.log('Connected to PTY'),
onDisconnect: () => console.log('Disconnected'),
});
useEffect(() => {
if (containerRef.current && terminal) {
terminal.mount(containerRef.current);
connect();
}
return () => ();
}, [terminal]);
(
);
}
Programmatic Input/Output
import { useTerminal } from '@wterm/react';
export default function ProgrammaticTerminal() {
const { terminal, write } = useTerminal({
wasmUrl: '/wterm.wasm',
});
const runCommand = () => {
write('\x1b[32mHello, world!\x1b[0m\r\n');
write('\x1b[1mBold text\x1b[0m\r\n');
};
return (
<>
<div ref={(el) => el && terminal?.mount(el)} style={{ height: 300 }} />
<button onClick={runCommand}>Write to terminal</button>
</>
);
}
Vanilla JS Usage
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="node_modules/@wterm/dom/dist/wterm.css" />
</head>
<body>
<div id="terminal" style="width:800px;height:500px"></div>
<script type="module">
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({
container: document.getElementById('terminal'),
wasmUrl: '/wterm.wasm',
wsUrl: 'ws://localhost:3001/pty',
theme: 'solarized-dark',
scrollback: 2000,
});
term.connect();
term.write('\x1b[33mWelcome!\x1b[0m\r\n');
term.resize(120, );
In-Browser Bash (No Backend)
import { Terminal } from '@wterm/react';
import { JustBashTransport } from '@wterm/just-bash';
export default function BrowserShell() {
return (
<Terminal
transport={new JustBashTransport()}
wasmUrl="/wterm.wasm"
theme="default"
style={{ width: '100%', height: '500px' }}
/>
);
}
Themes
Built-in themes: default, solarized-dark, monokai, light
<Terminal theme="monokai" wasmUrl="/wterm.wasm" wsUrl="..." />
Custom Theme via CSS Custom Properties
#terminal {
--wterm-bg: #0d1117;
--wterm-fg: #c9d1d9;
--wterm-cursor: #58a6ff;
--wterm-selection-bg: rgba(88, 166, 255, 0.3);
--wterm-color-0: #161b22;
--wterm-color-1: #ff7b72;
--wterm-color-2: #3fb950;
--wterm-color-3: #d29922;
--wterm-color-4: #58a6ff;
--wterm-color-5: #bc8cff;
--wterm-color-6: #39c5cf;
--wterm-color-7: #b1bac4;
}
WebSocket PTY Backend (Node.js)
import { WebSocketServer } from 'ws';
import * as pty from 'node-pty';
const wss = new WebSocketServer({ port: 3001, path: '/pty' });
wss.on('connection', (ws) => {
const shell = pty.spawn(process.env.SHELL || 'bash', [], {
name: 'xterm-256color',
cols: 80,
rows: 24,
cwd: process.env.HOME,
env: process.env as Record<string, string>,
});
shell.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(Buffer.from(data, 'binary'));
}
});
ws.on(, {
text = msg.();
{
parsed = .(text);
(parsed. === ) {
shell.(parsed., parsed.);
;
}
} {}
shell.(text);
});
ws.(, shell.());
shell.( ws.());
});
Next.js Integration
npm install @wterm/react @wterm/core
cp node_modules/@wterm/core/wterm.wasm public/
'use client';
import dynamic from 'next/dynamic';
const WTerminal = dynamic(
() => import('@wterm/react').then((m) => m.Terminal),
{ ssr: false }
);
export default function TerminalPage() {
return (
<WTerminal
wsUrl={process.env.NEXT_PUBLIC_PTY_WS_URL}
wasmUrl="/wterm.wasm"
theme="monokai"
style={{ width: '100%', height: '600px' }}
/>
);
}
Markdown in Terminal
import { renderMarkdown } from '@wterm/markdown';
import { createTerminal } from '@wterm/dom';
const term = await createTerminal({ container, wasmUrl: '/wterm.wasm' });
const md = `# Hello\n\n**Bold** and *italic* text.\n\n\`\`\`js\nconsole.log('hi');\n\`\`\``;
term.write(renderMarkdown(md));
Configuration Reference
createTerminal / useTerminal Options
| Option | Type | Default | Description |
|---|
wasmUrl | string | '/wterm.wasm' | Path to WASM binary |
wsUrl | string | — | WebSocket PTY endpoint |
transport | Transport | — | Custom transport (overrides wsUrl) |
theme | string | 'default' | Built-in theme name |
scrollback | number | 1000 | Scrollback buffer rows |
cols | number | auto | Initial column count |
rows | number | auto | Initial row count |
onData | (data: string) => void | — | Raw output callback |
onConnect | () => void | — | Connection established |
onDisconnect | () => void | — | Connection closed |
onResize | (cols, rows) => void | — | Resize event callback |
Development Setup
npm install -g portless
git clone https://github.com/vercel-labs/wterm
cd wterm
pnpm install
zig build
zig build -Doptimize=ReleaseSmall
pnpm build
zig build test
cd web && python3 -m http.server 8000
cp web/wterm.wasm examples/nextjs/public/
pnpm --filter nextjs dev
Troubleshooting
WASM file not found (404)
cp node_modules/@wterm/core/wterm.wasm public/
Terminal not rendering / blank screen
- Container must have explicit
width and height (not auto)
- Wrap in
dynamic(..., { ssr: false }) in Next.js — WASM requires browser APIs
- Check browser console for WASM instantiation errors
WebSocket connection refused
- Verify PTY backend is running and
wsUrl matches
- Check CORS headers if backend is on a different origin
- Use
wss:// for HTTPS-served apps
Text selection / copy not working
- wterm uses DOM rendering, so native browser selection should work
- Ensure the container does not have
user-select: none in CSS
Resize not working
- wterm uses
ResizeObserver automatically; ensure the container resizes with the page
- For manual resize:
terminal.resize(cols, rows)
Alternate screen apps (vim, htop) display incorrectly
- Ensure your PTY backend sets
TERM=xterm-256color
- Verify the WebSocket sends binary (not UTF-8 string) frames