Ghostty VT for the web

A real Ghostty terminal, running in the browser.

Gespenst drives the official Ghostty VT engine from TypeScript: unmodified WASM parsing, worker isolation, byte-native streams, and much more.

MIT licensedESM only

Checking browser runtime

A real Bash process compiled to WASIX, with coreutils on its PATH, running in this tab with no backend.

Official Ghostty core The unmodified nightly ghostty-vt.wasm, checksummed and ABI-verified before a terminal is created.
Off the main thread Parsing and rendering run in a dedicated worker by default, with shared-worker and main-thread fallbacks.
Bytes end to end PTY output stays Uint8Array from the socket to Ghostty. Nothing is decoded to strings on the way.
Shells without a server WASIX packages run in the page, so a working shell needs no PTY host and no deployment.

Start with the native API.

Keep PTY bytes intact from the socket to Ghostty. Framework adapters wrap the same terminal, and compatibility is added only where an existing integration needs it.

Read the getting-started guide
pnpm add @gespenst/core
import { createTerminal } from '@gespenst/core';
import '@gespenst/core/style.css';

const terminal = await createTerminal({
  container: document.querySelector('#terminal'),
  worker: 'dedicated',
});
pnpm add @gespenst/core @gespenst/react
import { GespenstTerminal } from '@gespenst/react';
import '@gespenst/core/style.css';

export function Shell() {
  return <GespenstTerminal onReady={(terminal) => terminal.focus()} />;
}
pnpm add @gespenst/core @gespenst/vue
<script setup lang="ts">
import { GespenstTerminal } from '@gespenst/vue';
import '@gespenst/core/style.css';
</script>

<template><GespenstTerminal @ready="terminal => terminal.focus()" /></template>
pnpm add @gespenst/core @gespenst/svelte
<script lang="ts">
  import { gespenstTerminal } from '@gespenst/svelte';
  import '@gespenst/core/style.css';
</script>

<div use:gespenstTerminal={{ onReady: (terminal) => terminal.focus() }} />

Know what runs where.

Gespenst separates process transport, VT state, and rendering. Choose each boundary independently without translating terminal output or adopting a prescribed backend.

Process

PTY or WASIX

Run a server shell or a browser-only package.

node-pty | @gespenst/wasmer
Transport

Native byte streams

Preserve PTY output and input as binary data.

ReadableStream<Uint8Array>
Runtime

Ghostty VT state

Parse modes, replies, scrollback, selection, and snapshots.

worker: 'dedicated'
Surface

Browser renderer

Paint retained frames and collect browser input.

WebGPU | WebGL2 | Canvas 2D
Output moves from the process to the renderer. Keyboard, paste, pointer, terminal replies, and resize events return through the same byte-oriented boundary.

Pick execution by terminal count.

The default selects a dedicated worker when the browser supports it and falls back safely.

'dedicated'Recommended
One isolated worker per terminal. Use this for most applications and single-session views.
'shared'Many sessions
Multiplex terminals through one worker when worker count matters. Busy sessions share its event loop.
falseMain thread
Useful for tests, constrained embeds, and unsupported environments. Parsing competes with application work.
Compare configuration tradeoffs

One core, composable parts.

Every package ships as ESM with lockstep versions, so nothing you skip ends up in the bundle. Each entry links to its generated API reference.

Guides for the whole path.

The core guide walks from first render to production concerns: transports, configuration, fonts, permissions, performance, and the failure modes worth knowing early.