termdom

Build Terminal UIs with HTML, CSS and DOM.

npm install @b9g/termdom

a real DOM, a real cascade, a real layout engine โ€” rendered to your terminal

TermDOM is a JavaScript library that displays HTML and CSS in the terminal. It draws actual DOM nodes to terminal output and redraws the screen when they mutate, so TUIs and interactive CLIs can be written with vanilla JavaScript or any frontend web framework.

Klondike solitaire โ€” examples/solitaire.ts

import {TermDOM} from "@b9g/termdom";

const term = new TermDOM();
term.attach();

// The document is a real DOM document.
const {document} = term;
document.body.innerHTML = `
  <style>
    .card { border: 1px solid #5fafff; padding: 0 1ch; width: 36ch; }
    .title { color: #5fafff; font-weight: bold; }
    .done { color: green; }
    .rest { color: #444; }
    .pct { color: #888; }
  </style>
  <div class="card">
    <div class="title">Installing</div>
    <div>
      <span class="done" id="done"></span><span class="rest" id="rest"></span>
      <span class="pct" id="pct"></span>
    </div>
  </div>
`;

// TermDOM observes mutations and re-renders automatically.
let n = 0;
setInterval(() => {
  n = (n + 1) % 101;
  const cells = Math.round(n / 4);
  document.getElementById("done").textContent = "โ–ˆ".repeat(cells);
  document.getElementById("rest").textContent = "โ–‘".repeat(25 - cells);
  document.getElementById("pct").textContent = String(n).padStart(3) + "%";
}, 50);

The card above, running

One cell is 1ch wide and 1px tall. Every box lands on whole cells.

Write a web page. Get a TUI. #

Every glyph below is a DOM element. The spinner is a <span> whose textContent mutates; painting is automatic, like the browser.

const spinner = document.createElement("span");
spinner.className = "spin";           // .spin { color: green }
section.appendChild(spinner);
setInterval(() => {
  spinner.textContent = frames[n++ % frames.length];
}, 30);                               // no render call -- mutations paint

examples/animated.ts

Interactivity is just DOM events. #

A NERDTree-style file browser in ~200 lines of vanilla DOM: querySelectorAll for the rows, classList for the selection, keydown for the keys, and scrollIntoView() to move the camera.

document.addEventListener("keydown", (ev) => {
  if (ev.key === "j") select(selected + 1);
  if (ev.key === "Enter") expand(rows()[selected]);
});
rows()[selected].scrollIntoView();

examples/tree.ts

Real text input. Real caret. Real IME. #

<input> elements with focus traversal and :focus styling โ€” and the caret is the real terminal cursor, so CJK input methods compose in the field, measured in cells.

<div class="field">
  <div class="label">Name</div><input id="name">
</div>

field.addEventListener("input", updatePreview);

examples/form.ts

Features #

Compatibility #

The compatibility matrix is generated by a probe suite: each DOM API, selector, and CSS property is applied to a real document and rendered, and the table records whether the output changed.

Get started #

npm install @b9g/termdom

Guides โ†’ ยท Examples on GitHub โ†’

Name #

Not to be confused with DomTerm, Per Bothner's terminal emulator built out of DOM elements. The two projects are each other's inverse: DomTerm puts a terminal in the DOM; TermDOM puts the DOM in a terminal.