@xterm/xterm
@xterm/xterm is a browser terminal emulator: it interprets terminal control sequences, maintains screen and scrollback buffers, renders text, handles keyboard, mouse, selection, IME, Unicode, and accessibility behavior, and exposes input and resize events. It is the display and interaction half of a web terminal, not a shell or process runner. A real interactive session still needs a server-side pseudoterminal, such as node-pty, plus a transport that sends process output to Terminal.write and terminal input back to the process.
The default serious choice for a full VT-style terminal in a browser, backed by years of use in major developer tools. Do not mistake the polished renderer for a complete remote-shell system; the backend PTY, transport, access control, resizing, lifecycle, and reconnect behavior are your project.
Use it if
- You are building a browser IDE, SSH console, container exec view, serial console, or terminal log viewer that must understand VT control sequences
- You need the terminal engine used by projects such as VS Code, JupyterLab, Hyper, and browser-based infrastructure consoles
- You need selectable output, keyboard and mouse input, CJK and IME support, theming, screen-reader mode, and configurable contrast
- You want an addon system for fitting, WebSocket attachment, search, links, WebGL rendering, serialization, clipboard, images, or richer Unicode
- You expect an installable terminal or a shell: the README explicitly says xterm.js is not bash and must be connected to a process through a separate pseudoterminal layer
- You need a Node.js-only parser or buffer with no DOM: the project publishes @xterm/headless for that use case instead of the browser package
- You must support old or frozen browsers: official support is limited to the latest Chrome, Edge, Firefox, and Safari releases
- You want fitting, URL detection, search, clipboard, serialization, WebGL, images, or advanced Unicode in the core install: each is a separately installed and loaded addon
- You depend on experimental APIs or beta builds without tracking release notes: the README says experimental APIs may change radically, and beta releases can contain bugs or breaking changes
Setup reality
The npm package has no runtime dependencies, ships TypeScript declarations plus CommonJS and ESM files, and installs with npm install @xterm/xterm. You still must import @xterm/xterm/css/xterm.css; without it, the internal viewport, selection, helper textarea, and rows will not lay out correctly. The host element needs real dimensions before terminal.open runs. Most applications also install @xterm/addon-fit, load it, and call fit after the container becomes visible and after fonts settle. xterm.js does not launch bash, SSH, or containers. A server must own a pseudoterminal or other process, authenticate access, transport output to terminal.write, accept onData input, and apply onResize dimensions back to the PTY. A WebSocket only carries bytes; it does not provide process isolation, authorization, reconnect state, or flow control. For reconnectable sessions, the README points to @xterm/headless plus the serialize addon as one way to preserve terminal state on the process host. The browser package should be created only after a DOM node exists, so React, Next.js, and SSR integrations need a client effect and cleanup. Dispose the terminal, addons, ResizeObserver, WebSocket listeners, and every event disposable when the view unmounts. Addons must match the core major, and version 6 removed the old canvas renderer in favor of the default DOM renderer or @xterm/addon-webgl. Only enable screen input when intended; disableStdin is useful for read-only logs. Test keyboard shortcuts, IME, screen readers, font loading, WebGL context loss, resize loops, and mobile viewport behavior on actual target browsers.
Patterns
Open a terminal and write styled textopen-basic-terminal
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
const host = document.querySelector<HTMLDivElement>('#terminal')!;
const terminal = new Terminal({ cursorBlink: true });
terminal.open(host);
terminal.write('Hello from \x1b[1;32mxterm.js\x1b[0m\r\n');The host must have a non-zero size, and the package CSS is required for correct terminal layout. Use carriage return plus newline for a new line at column zero.
Wire terminal input and output to a WebSocketconnect-websocket-manually
const socket = new WebSocket('wss://example.com/terminal/session-123');
socket.binaryType = 'arraybuffer';
const input = terminal.onData((data) => {
if (socket.readyState === WebSocket.OPEN) socket.send(data);
});
socket.addEventListener('message', (event) => {
terminal.write(typeof event.data === 'string' ? event.data : new Uint8Array(event.data));
});The server still needs an authenticated PTY or process. Preserve byte encoding end to end, and dispose the onData subscription when the session closes.
Use the official WebSocket attach addonattach-websocket-addon
import { Terminal } from '@xterm/xterm';
import { AttachAddon } from '@xterm/addon-attach';
const terminal = new Terminal();
const socket = new WebSocket('wss://example.com/terminal/session-123');
const attach = new AttachAddon(socket);
terminal.loadAddon(attach);Install @xterm/addon-attach separately. The addon connects messages, but authentication, process ownership, reconnection, and server cleanup remain application concerns.
Refit when the host element changes sizefit-to-container
import { FitAddon } from '@xterm/addon-fit';
const fit = new FitAddon();
terminal.loadAddon(fit);
terminal.open(host);
fit.fit();
const observer = new ResizeObserver(() => fit.fit());
observer.observe(host);Install @xterm/addon-fit separately. Avoid fitting a hidden or zero-size element, and disconnect the ResizeObserver during cleanup.
Send terminal dimensions to the backendresize-backend-pty
const resize = terminal.onResize(({ cols, rows }) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'resize', cols, rows }));
}
});The backend must apply these dimensions to its pseudoterminal. Fitting only the browser grid leaves full-screen programs such as vim and tmux at the old size.
Detect web links with the official addonadd-clickable-links
import { WebLinksAddon } from '@xterm/addon-web-links';
const links = new WebLinksAddon((event, uri) => {
event.preventDefault();
if (new URL(uri).protocol === 'https:') window.open(uri, '_blank', 'noopener,noreferrer');
});
terminal.loadAddon(links);Treat terminal output as untrusted. Validate allowed protocols and open new tabs with noopener and noreferrer rather than blindly navigating to detected text.
Search terminal scrollbacksearch-buffer
import { SearchAddon } from '@xterm/addon-search';
const search = new SearchAddon();
terminal.loadAddon(search);
const found = search.findNext('error', {
caseSensitive: false,
wholeWord: true,
});Install @xterm/addon-search separately. Search operates on the terminal buffer and selection, not on the original process log outside retained scrollback.
Load WebGL rendering with a fallbackenable-webgl-renderer
import { WebglAddon } from '@xterm/addon-webgl';
const webgl = new WebglAddon();
webgl.onContextLoss(() => {
webgl.dispose();
});
try {
terminal.loadAddon(webgl);
} catch {
webgl.dispose(); // terminal keeps the default DOM renderer
}Version 6 removed the old canvas renderer. WebGL is optional, requires WebGL2, and should fall back to the built-in DOM renderer on setup failure or context loss.
Use the terminal as a read-only log viewercreate-read-only-log
const terminal = new Terminal({
disableStdin: true,
convertEol: true,
scrollback: 5000,
});
terminal.open(host);
terminal.write(logChunk);convertEol moves the cursor to column zero on line feeds, which is helpful for plain logs but can change intentional terminal output semantics.
Enable screen-reader output and contrast checksconfigure-accessibility
const terminal = new Terminal({
screenReaderMode: true,
minimumContrastRatio: 4.5,
theme: {
background: '#111827',
foreground: '#f9fafb',
cursor: '#fbbf24',
},
});Screen-reader mode has a rendering cost. Test the full keyboard and announcement flow with the assistive technologies your users actually run.
Reserve an application keyboard shortcutintercept-keyboard-shortcut
terminal.attachCustomKeyEventHandler((event) => {
const isCopy = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c';
if (isCopy && terminal.hasSelection()) {
navigator.clipboard.writeText(terminal.getSelection());
return false;
}
return true;
});Returning false prevents xterm.js from processing the event. Do not steal Ctrl+C when there is no selection, because terminal users expect it to send an interrupt.
Clean up a terminal and its integrationsdispose-terminal-session
function closeTerminal() {
observer.disconnect();
input.dispose();
resize.dispose();
attach.dispose();
socket.close(1000, 'terminal closed');
terminal.dispose();
}Dispose event registrations, addons, observers, transport listeners, and the Terminal itself. Also tell the server to release its process or session if closing the socket does not do that automatically.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @xterm/headless | npm | Choose it to parse and retain terminal state in Node.js without rendering a browser UI. |
| jquery.terminal | npm | Choose it for a command-interpreter UI with prompts and commands when full PTY terminal emulation is unnecessary. |
| hterm-umdjs | npm | Choose it when Chromium's hterm behavior or a UMD package is a firm compatibility requirement. |
| terminal-kit | npm | Choose it to build an interactive terminal application that runs in a real Node.js TTY rather than in a browser. |