jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Terminal

ghostty-term — the live wasm terminal

The brand's live terminal surface: a canvas grid rendered by the real libghostty-vt wasm, not a DOM reimplementation. The component owns painting and geometry — DPR-aware cells derived from the density tokens, rAF-batched dirty-row repaints — while the consumer owns the pty: onData carries terminal input out (keys, gated pastes, wheel) and the bind:this write() feeds pty output back in. Load failures degrade to a data-state machine with a terminal-styled fallback, or the consumer's own face through the children slot.

install — ghostty-term

$npx jixoai-ui add ghostty-term

https://ui.jixoai.com/r/ghostty-term.json

the wasm supply step: see Install prerequisites below

real libghostty-vt wasm rAF dirty-row canvas painting density-derived cell metrics onData input bridge typed error degradation

install

Install prerequisites

The one jixoai component with a supply-chain step: the wasm never enters git or your bundle source — the vite plugin pins its sha256, resolves it (env → cache → verified download), and hands the URL over through the virtual:jixoai-ghostty module. Two steps on a tailwind v4 + vite project:

1 — the jixoai base (tw4)
terminal
# 1 — the jixoai base on a tailwind v4 + vite project
npx jixoai-ui init --hue 330
components.json
{
  "registries": {
    "@jixoai": "https://ui.jixoai.com/r/{name}.json"
  }
}
terminal
# ghostty-term pulls its registry deps (ghostty-vt, color-utils) with it
npx jixoai-ui add ghostty-term
2 — the vite plugin (wasm supply)
terminal
# 2 — the wasm supply chain (the one component that needs it)
npm i -D @jixoai/ui-vite-plugin
vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { jixoai } from '@jixoai/ui-vite-plugin';
import tailwindcss from '@tailwindcss/vite';

export default {
  plugins: [sveltekit(), tailwindcss(), ...jixoai()],
};
src/vite-env.d.ts
/// <reference types="@jixoai/ui-vite-plugin/client" />

overview

Overview

The live terminal surface: a DPR-aware canvas grid painted by the real libghostty-vt wasm — the component owns paint and geometry, the consumer owns the pty, and the color boundary is deliberately one-sided.

Nothing re-implements VT here: parsing, grid state and scrollback live inside libghostty-vt; the component adds the browser-shaped pieces — DPR-aware cell metrics derived from the density kernels, rAF-batched dirty-row repaints, and the input bridge. The pty is yours: onData carries terminal input out (encoded keys, gated pastes, wheel), and the bind:this write() feeds pty output back in — the page's demo is a loopback answering through exactly that seam, no hidden shell.

Degradation is a typed state machine: data-state goes loading → ready | error, a failed wasm load renders a polite role="status" face carrying the GhosttyVTError message, and the children slot hands the degraded face to the consumer. The supply chain is the install section's contract — the wasm never enters git or the bundle source; the vite plugin pins its sha256 and serves it through the virtual:jixoai-ghostty module.

The axes story is this family's signature: SEVEN of the eight axes take lanes — density consumed as the cell-metric kernels (the one live ladder), size the §11 echo, the rest stamp-and-supply — and the eighth is the §13 shadow case: the theme prop is the shell-color OBJECT (absent-slot escape hatch), never the light/dark/system enum. The surface is typed-frozen under scoped .dark by design and the content is ANSI verbatim — a terminal keeps its colors. Kinship: terminal-card (the chrome paint law this page's window borrows) and code-card (the readonly render-side sibling).

ghostty-term

The live loopback: click the terminal and type — the fake shell on this page answers through onData/write with zero network. help · color · clear · ↑ history.

jixoai — ghostty-term
ghostty-term-usage.svelte svelte
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
  import type { GhosttyTermHandle } from '@ui/ghostty-term';

  let term = $state<GhosttyTermHandle>();
  const enc = new TextEncoder();
</script>

<!-- the container derives the grid (ResizeObserver → cols/rows) -->
<div class="h-80">
  <GhosttyTerm
    bind:this={term}
    onData={(bytes) => ptyInput.write(bytes)}
  />
</div>

<!-- write() is the pty OUTPUT side: rendered bytes feed back in -->
<button onclick={() => term?.write(enc.encode('\u001b[1mhello\u001b[0m\r\n'))}>
  write
</button>

demo

Failure & degradation

Loading is a state machine — data-state goes loading → ready | error — and a failed wasm load never crashes the page. Without a slot, the default fallback face is a terminal-styled status line; with the children slot, the consumer owns the degraded face outright.

ghostty-term · degradation

default fallback (no slot)
role="status" face — the typed GhosttyVTError message names the failure
children slot (consumer face)
consumer fallback — this face is the children slot
the slot also covers loading/ready as a plain overlay
ghostty-term-degradation-demo.svelte svelte
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
</script>

<!-- a failed wasm load never crashes the page: without a slot the
     default fallback face is a terminal-styled status line -->
<div class="h-40">
  <GhosttyTerm wasmUrl="https://invalid.jixoai.test/ghostty-vt.wasm" />
</div>

<!-- with the children slot, the consumer owns the degraded face outright -->
<div class="h-40">
  <GhosttyTerm wasmUrl="https://invalid.jixoai.test/ghostty-vt.wasm">
    <div
      class="absolute inset-0 flex items-center justify-center p-4 font-mono text-[13px] leading-5 text-terminal-foreground"
      role="status"
    >
      <span class="text-primary mr-2" aria-hidden="true">$</span>
      consumer fallback — this face is the children slot
    </div>
  </GhosttyTerm>
</div>

demo

Density & theming

Density rides the standard token kernels: --jx-text sets the cell font, --jx-line sets the row pitch — the same explicit density prop as every jixoai component. Theming is deliberately one-sided: the theme prop (and the --terminal tokens) restyle only the SHELL paper/ink; ANSI 8/256/truecolor content colors pass through the wasm verbatim.

ghostty-term · density

density prop — cell metrics follow
theme — shell only, ANSI verbatim
ghostty-term-density-demo.svelte svelte
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
</script>

<!-- the density prop: --jx-text sets the cell font, --jx-line the pitch -->
<div class="h-32"><GhosttyTerm density="sm" /></div>
<div class="h-32"><GhosttyTerm /></div>
<div class="h-32"><GhosttyTerm density="lg" /></div>

<!-- theme restyles only the SHELL paper/ink; ANSI passes verbatim -->
<div class="h-[9.5rem]"><GhosttyTerm /></div>
<div class="h-[9.5rem]">
  <GhosttyTerm theme={{ background: '#141019', foreground: '#f0e6ff' }} />
</div>

law

The wasm is the terminal

Nothing re-implements VT here. The platform gives parsing, grid state and scrollback inside libghostty-vt; this component adds exactly the browser-shaped pieces around it — DPR-aware painting, density-derived metrics, the input bridge, the degradation machine.

  • painting is data, not decoration: rAF batches writes, only dirty rows repaint, a row cache serves full repaints (theme/font changes) — no loops, no blink
  • the color boundary is one-sided by design: shell paper/ink resolve from jixoai tokens; content colors leave the wasm verbatim — theming never rewrites user output
  • the pty is yours: onData out, write in — the component never guesses what a shell is (this page’s demo is a loopback, not hidden behavior)
  • V1 bounds, stated plainly: no cursor/selection paint (the frozen vt face exposes no cursor read), no hyperlink activation, viewport-only scroll, and a clamped wheel shift where the upstream render state under-reports dirty rows

usage

Usage

Auto sizing fills the container; explicit cols/rows fixes the grid. The consumer owns the pty loop: onData out, write in.

GhosttyTerm usage
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
  import type { GhosttyTermHandle } from '@ui/ghostty-term';

  let term = $state<GhosttyTermHandle>();
  const enc = new TextEncoder();
</script>

<!-- the container derives the grid (ResizeObserver → cols/rows) -->
<div class="h-80">
  <GhosttyTerm
    bind:this={term}
    onData={(bytes) => ptyInput.write(bytes)}
  />
</div>

<!-- write() is the pty OUTPUT side: rendered bytes feed back in -->
<button onclick={() => term?.write(enc.encode('\u001b[1mhello\u001b[0m\r\n'))}>
  write
</button>
the pty loop
pty output ─▶ handle.write(bytes) ─▶ wasm vtWrite ─▶ dirty rows ─▶ canvas paint
keys/paste ─▶ keyEncode + paste gate ─▶ onData(bytes) ─▶ your pty

api

API

Props from the GhosttyTerm Props interface; the bind:this surface mirrors a pty handle. Rest props spread onto the root. The generated meta carries 27 props (26 named + the synthesized rest) — the hand table serves the 11 consumer rows, with density and theme riding the EXTRA lane (reference identity): density because the family own 'default' is contract, theme because the generic light/dark/system enum is SHADOWED by the shell-object escape hatch — the carriers-bijection ruling, task 27.

Properties

PropertyTypeDefaultDescription
cols number—Fixed grid columns; any explicit cols/rows (or auto={false}) switches out of auto sizing.
rows number—Fixed grid rows.
auto booleantrueDerive the grid from the container box (ResizeObserver).
fontSize number--jx-textCell font size in px — must be finite positive; anything else warns once and falls back to the density token.
wasmUrl stringvirtual:jixoai-ghosttywasm asset URL; the default resolves the vite plugin’s virtual module (the install prerequisite).
theme { background?, foreground?, … }terminal tokensShell overrides ONLY — the ABSENT-SLOT escape hatch (no theme = the jixoai token sheet owns every shell color, and the surface is typed-frozen: a scoped .dark moves nothing, measured). ANSI/256/truecolor CONTENT colors are never themed through this prop. The generic light/dark/system THEME AXIS is shadowed by this object — see the axes table.
onData (bytes: Uint8Array) => void—Terminal INPUT out: encoded keys, gated pastes (bind:this write feeds OUTPUT in).
onResize (detail: { cols, rows }) => void—Fires when the auto-mode grid derivation changes.
density Density'default' · ambient scopeExplicit density for the cell metric kernels (--jx-text cell font, --jx-line row pitch — measured 12/13/15px and 18/20/24px across sm/default/lg). Omitted → the ambient density scope, else the family own 'default' (the always-concrete cell math).
class string''Merged onto the root through cn().
children Snippet—Overlay slot; when provided it also replaces the default error fallback face.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

Handle (bind:this)

PropertyTypeDefaultDescription
write (bytes: Uint8Array) => void—Feed pty OUTPUT into the terminal — rAF-batched with in-flight writes.
reset () => void—Full reset (RIS) back to a pristine grid.
resizeTo (cols: number, rows: number) => void—Imperative grid resize; an auto-mode container change overrides it on the next derivation.
snapshot () => string—Base64 terminal snapshot for diagnostics/tests (V1: encode only).

axes

The eight axes on ghostty-term

SEVEN of the eight axes take lanes — size · shape · radius · density · color · elevation · motion — and the eighth is the §13 shadow case that names the ruling: the family's own `theme` prop is the shell-color OBJECT (the absent-slot escape hatch), so the generic light/dark/system enum has no seat. Density is the one consumed ladder (the cell-metric kernels, measured 12/13/15px + 18/20/24px); size is the §11 echo with the fixed error voice as its nothing-follows proof; the shell is typed-frozen under scoped .dark by design — a terminal is deliberately theme-independent; everything else stamps-and-supplies. fontSize keeps its component-specific name (no collision, §13).

PropertyTypeDefaultDescription
density 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings)'default' · ambient scopeCONSUMED — the cell-metric kernels ARE the density contract: the cell font rides --jx-text and the row pitch --jx-line, measured across the demo rungs (kernel ladder 12 / 13 / 15px text, 18 / 20 / 24px line at sm/default/lg; the rung stamps data-density on the family root). The family own 'default' is the always-concrete cell math — a terminal never renders unspecified. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'THE §11 ECHO — the stamp lands verbatim (measured root inline: --jx-size-effective: 13px; font-size: var(--jx-size-effective, 1rem); computed 13px / 18px at the demo lanes) and the cell math follows NOTHING of it: the canvas font is fontSize → --jx-text (density-governed) and the error face is the FIXED --jx-text-base voice (measured 13px at both size lanes — the nothing-follows proof). A div carries no size-like attribute (the native-wrapper §1 rule). Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — stamps --jx-shape-effective + --jx-radius-factor-effective; no family css reads them (grep receipt: zero carrier reads in ui/ghostty-term/ + vt-deps.ts). The surface is square by the terminal law. Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-radius-effective; zero readers (grep receipt: no border-radius anywhere in the family css — the region root is the §3 concentric ANCHOR for composed chrome, the supply is the point). Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SUPPLY-ONLY — stamps --jx-color-effective; zero readers (grep receipt). The shell ink/paper are the typed terminal tokens; the error prompt is --jx-primary; content colors are the wasm’s ANSI/256/truecolor, verbatim. Number unit: hue degrees.
theme SHADOWED — the shell-theme OBJECT, not the axis enumterminal tokens (absent = the sheet owns the shell)THE SHADOWED SLOT (the §13 case this campaign named the ruling after): the family's theme prop is the ABSENT-SLOT escape hatch — a structured { background, foreground, selectionBackground, … } object; ABSENCE is the meaningful state (the token sheet owns every shell color). 'light'/'dark'/'system' are not lanes here. THE MEASURED SPLIT: the surface atom is a typed :root token; the canvas shell probes the RAW shell tokens live but unobserved — a scope flip moves neither (measured: under a scoped .dark the ground holds oklch(0.9551 0 0) and ink oklch(0 0 0) while the scope's own --terminal reads the dark pole; the CSS surface holds by alias-freeze, the canvas by no-observer — there is no MutationObserver on class/data-theme, so the cached raw value paints until a theme-prop change re-probes the then-current scope), and the CONTENT is the wasm's ANSI — absolute under any theme. A terminal is deliberately theme-independent: the surface typed-frozen by construction, the shell pole-holding by no-observation, ANSI-verbatim by contract. No number lane, no enum lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-elevation-effective; zero readers in the family css (grep receipt: zero shadow declarations — the demo window’s float shadow belongs to the PAGE chrome, --shadow-md on the demo shell, not the component). Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'SUPPLY-ONLY — stamps --jx-motion-effective; zero readers (grep receipt). The family declares no transitions; the cursor blink is the wasm’s own state machine and painting is rAF-batched data flow, not choreography. Number unit: coefficient.

Receipts: the density kernel ladder, the size echo (with the fixed --jx-text-base error voice), and the typed-frozen split were measured on this page's served DOM (probe, task 27); the supply-only rows carry grep receipts over ui/ghostty-term/ and vt-deps.ts. The adoption is the census batch A row (explicit-props W3-A); the theme shadow is the §13 no-rename precedent (the absent-slot contract, ghostty-term-defaults.svelte.ts).

one real query() case
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- below 48rem the base (13px) applies; at 48rem+ the md case (18px)
     wins — the §11 echo the root stamps (the cell math follows the
     density kernels, not this stamp) -->
<GhosttyTerm rows={3} cols={40} size={query({ md: 18 }, 13)} wasmUrl="https://invalid.jixoai.test/ghostty-vt.wasm" />

ghostty-term · query()

Media keys are min-width: below 48rem the base applies — the root stamp reads 13px; at 48rem and wider the md case wins — 18px. The number lane goes bare (results infer); string lanes take both generics. Resize across 48rem.

ghostty-term-query-demo.svelte svelte
<script lang="ts">
  import GhosttyTerm from '@ui/ghostty-term';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- below 48rem the base (13px) applies; at 48rem+ the md case (18px)
     wins — the §11 echo the root stamps (the cell math follows the
     density kernels, not this stamp) -->
<GhosttyTerm rows={3} cols={40} size={query({ md: 18 }, 13)} wasmUrl="https://invalid.jixoai.test/ghostty-vt.wasm" />

ghostty-term · universal props

ghostty-term-universal.svelte svelte
<!-- ghostty-term: 7 of the 8 axes — 'theme' stays the family's own
     shell-theme OBJECT (background/foreground/…), never the axis enum;
     fontSize keeps its component-specific name (no collision) -->
<GhosttyTerm rows={3} cols={40} size={13} density="small" />

a11y

Accessibility

The canvas is a raster — semantics live one level up on the focused root; failures announce politely through a live status face.

ARIA

AttributeValueDescription
tabindex="0"rootThe root div is the keyboard surface (keydown → keyEncode); the canvas itself is aria-hidden.
aria-labelterminalNames the surface for assistive tech — pass your own through the rest props spread.
role="status"error faceThe default degradation fallback renders as a polite status line carrying the typed error message.
data-stateloading | ready | errorThe load state machine exposed as an attribute — style or assert it without touching internals.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h