wterm-web-terminal
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings
Best use case
wterm-web-terminal is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings
Teams using wterm-web-terminal should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/wterm-web-terminal/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How wterm-web-terminal Compares
| Feature / Agent | wterm-web-terminal | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# wterm Web Terminal Emulator
> Skill by [ara.so](https://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
```bash
# React
npm install @wterm/react @wterm/core
# Vanilla JS
npm install @wterm/dom @wterm/core
# In-browser bash (no backend needed)
npm install @wterm/just-bash @wterm/core
```
Copy the WASM binary to your public directory:
```bash
cp node_modules/@wterm/core/wterm.wasm public/
```
## React Usage
### Basic Terminal Component
```tsx
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
```tsx
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 () => disconnect();
}, [terminal]);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '400px', background: '#1e1e1e' }}
/>
);
}
```
### Programmatic Input/Output
```tsx
import { useTerminal } from '@wterm/react';
export default function ProgrammaticTerminal() {
const { terminal, write } = useTerminal({
wasmUrl: '/wterm.wasm',
});
const runCommand = () => {
// Write VT100 escape sequences or plain text
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
```html
<!-- index.html -->
<!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();
// Write directly
term.write('\x1b[33mWelcome!\x1b[0m\r\n');
// Resize programmatically
term.resize(120, 40); // cols, rows
</script>
</body>
</html>
```
## In-Browser Bash (No Backend)
```tsx
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`
```tsx
// Via prop
<Terminal theme="monokai" wasmUrl="/wterm.wasm" wsUrl="..." />
```
### Custom Theme via CSS Custom Properties
```css
#terminal {
--wterm-bg: #0d1117;
--wterm-fg: #c9d1d9;
--wterm-cursor: #58a6ff;
--wterm-selection-bg: rgba(88, 166, 255, 0.3);
/* ANSI colors */
--wterm-color-0: #161b22; /* black */
--wterm-color-1: #ff7b72; /* red */
--wterm-color-2: #3fb950; /* green */
--wterm-color-3: #d29922; /* yellow */
--wterm-color-4: #58a6ff; /* blue */
--wterm-color-5: #bc8cff; /* magenta */
--wterm-color-6: #39c5cf; /* cyan */
--wterm-color-7: #b1bac4; /* white */
/* bright variants: --wterm-color-8 through --wterm-color-15 */
}
```
## WebSocket PTY Backend (Node.js)
```ts
// server.ts — example PTY backend using node-pty
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>,
});
// PTY → client (binary framing)
shell.onData((data) => {
if (ws.readyState === ws.OPEN) {
ws.send(Buffer.from(data, 'binary'));
}
});
// Client → PTY
ws.on('message', (msg: Buffer) => {
const text = msg.toString('binary');
// wterm sends resize as JSON: {"type":"resize","cols":120,"rows":40}
try {
const parsed = JSON.parse(text);
if (parsed.type === 'resize') {
shell.resize(parsed.cols, parsed.rows);
return;
}
} catch {}
shell.write(text);
});
ws.on('close', () => shell.kill());
shell.onExit(() => ws.close());
});
```
## Next.js Integration
```bash
# Install
npm install @wterm/react @wterm/core
# Copy WASM to public
cp node_modules/@wterm/core/wterm.wasm public/
```
```tsx
// components/Terminal.tsx
'use client';
import dynamic from 'next/dynamic';
// Must be client-only — no SSR
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
```ts
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
```bash
# Prerequisites: Zig 0.15.2+, Node.js 20+, pnpm 10+
npm install -g portless
git clone https://github.com/vercel-labs/wterm
cd wterm
pnpm install
# Build WASM core
zig build # debug
zig build -Doptimize=ReleaseSmall # ~12 KB release
# Build all packages
pnpm build
# Run Zig tests
zig build test
# Serve vanilla demo
cd web && python3 -m http.server 8000
# Run Next.js example
cp web/wterm.wasm examples/nextjs/public/
pnpm --filter nextjs dev
# Available at: nextjs-example.wterm.localhost
```
## Troubleshooting
**WASM file not found (404)**
```bash
# Ensure wterm.wasm is in your public directory
cp node_modules/@wterm/core/wterm.wasm public/
# In Next.js, verify it's at public/wterm.wasm
```
**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) framesRelated Skills
syswatch-terminal-diagnostics
SysWatch is a single-host system diagnostics TUI for macOS and Linux with twelve tabs, plain-English insights, and session scrubbing.
sheets-terminal-spreadsheet
Terminal-based spreadsheet TUI tool written in Go for editing CSV files in the terminal with vim-like keybindings
rattles-terminal-spinners
Minimal terminal spinner library for Rust with preset collection and no-std support
ghostling-libghostty-terminal
Build minimal terminal emulators using the libghostty-vt C API with Raylib for windowing and rendering
cmux-terminal-multiplexer
AI-native terminal multiplexer with programmable socket API, full Playwright-equivalent browser automation, and agent team coordination — built for Claude Code and autonomous agent workflows
```markdown
---
zeroboot-vm-sandbox
Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot
yourvpndead-vpn-detection
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
xata-postgres-platform
Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment
x-mentor-skill-nuwa
AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.
wx-favorites-report
End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.
worldmonitor-intelligence-dashboard
Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking