TermDOM: терминальные интерфейсы на HTML и CSS
Технический разбор · Август 2026

TermDOM: терминальные интерфейсы
на HTML и CSS

А что, если бы ваш терминал был браузером? TermDOM рендерит настоящий DOM-документ — с каскадом CSS, flexbox, формами и веб-компонентами — прямо на экран терминала. Разбираемся, как это работает и что можно построить уже сегодня.

Олег Максимов 18 августа 2026 15 мин чтения

Введение

Долгое время создание терминального интерфейса означало изучение нового мира: деревья виджетов, вызовы раскладки, таблицы клавиш. Ink принёс React-компоненты в терминал. blessed дал классический набор виджетов. Но каждый подход заставлял думать в терминальных абстракциях, а не в тех веб-технологиях, которые вы уже знаете.

TermDOM идёт обратным путём: он реализует конвейер рендеринга браузера на сетке символьных ячеек вместо пикселей. В результате библиотека рендерит настоящий DOM-документ — тот самый, который вы меняете через document.querySelector() и innerHTML, — прямо в терминал. Node Weekly № 637 (13 августа 2026) вынес её в заголовок выпуска, а репозиторий активно обновлялся ещё 16 августа 2026 года, так что это свежий взгляд на быстро меняющийся проект.

В статье я разберу, что такое TermDOM на самом деле, как устроен его конвейер рендеринга, как шаг за шагом собрать небольшой TUI, где он находится рядом с Ink, blessed и clack, и что говорит матрица совместимости об ограничениях сегодня.

Идея: настоящий DOM в вашем терминале

TermDOM — это JavaScript-библиотека, которая отображает HTML и CSS в терминале. Она рисует реальные DOM-узлы в терминальном выводе и перерисовывает экран при их изменении, поэтому терминальные интерфейсы и интерактивные CLI можно писать на чистом JavaScript или с любым фронтенд-фреймворком. В README это сформулировано одной строкой: «Напишите веб-страницу — получите TUI».

Пакет в npm называется @b9g/termdom (версия 0.1.3 на момент написания статьи, распространяется как пре-релиз):

npm install @b9g/termdom

Минимальная программа выглядит так:

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

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

// document — настоящий DOM-документ.
const {document} = term;
document.body.innerHTML = `
  <style>
    .card { border: 1px solid #5fafff; padding: 0 1ch; width: 36ch; }
    .title { color: #5fafff; font-weight: bold; }
  </style>
  <div class="card">
    <div class="title">Привет, терминал</div>
    <div id="status">Готово</div>
  </div>
`;

Вот и всё. Библиотека перехватывает терминал, разбирает разметку, применяет стили и рисует результат. Каждый символ на экране — это DOM-элемент; каждое изменение DOM отражается автоматически. Здесь нет вызова рендера, нет ручного сравнения виртуального DOM — изменения отрисовываются сами, как в браузере.

Главное обещание

Одна ячейка — 1ch в ширину и 1px в высоту. Каждый блок ложится на целые ячейки. Вы пишете веб-страницу, TermDOM превращает её в TUI.

Как устроен конвейер рендеринга

TermDOM реализует конвейер рендеринга браузера на сетке символьных ячеек вместо пикселей. CSS-длины отображаются на сетку — 1px это одна строка, 1ch это один столбец — поэтому блочная модель, flexbox и таблицы раскладываются в целые ячейки. Вот полный цикл, как его описывает README:

  1. Стили. CSS из элементов <style> и атрибутов style каскадируется и наследуется, как в браузере, а затем переводится в ANSI-последовательности для цвета и оформления текста.
  2. Раскладка. Блочная модель, flexbox и таблицы вычисляют блоки в целых терминальных ячейках, с полями, рамками и внутренними отступами.
  3. Отрисовка. Движок рисует результат в буфер ячеек.
  4. Дифф. Новый буфер сравнивается с предыдущим кадром, и в stdout записывается только разница — ANSI-последовательности.
  5. Ввод. Ввод работает в обратную сторону: escape-последовательности из stdin декодируются в события клавиатуры, мыши и вставки, а затем отправляются DOM-узлам.

Именно перерисовка по мутациям делает библиотеку похожей на браузер. MutationObserver следит за документом; когда что-то меняется, движок пересчитывает стили и раскладку затронутых поддеревьев, перерисовывает и диффает. Привычные DOM-утилиты подключены к движку раскладки и вьюпорту: document.querySelector(), MutationObserver, ResizeObserver и Element.getBoundingClientRect() следуют стандартам браузера.

Упрощённая модель цикла кадров:

while (running) {
  // 1. Были ли мутации DOM? Их поставили в очередь колбэки MutationObserver.
  const changes = drainMutationQueue();

  if (changes.length > 0) {
    // 2. Пересчитываем стили затронутых поддеревьев (каскад + наследование).
    for (const node of changes) {
      recomputeStyle(node);   // CSS -> итоговые цвета и оформление
      layout(node);           // блочная модель, flexbox, таблицы -> ячейки
    }

    // 3. Рисуем буфер ячеек и сравниваем с прошлым кадром.
    const buffer = paint(document.body);
    const patch = diff(previousBuffer, buffer);

    // 4. Пишем в stdout только разницу как ANSI-последовательности.
    if (patch.length > 0) writeToStdout(patch);
    previousBuffer = buffer;
  }
}

Так как вывод — это дифф, спиннер, меняющий один <span> за кадр, обходится в несколько escape-последовательностей, а не в полную перерисовку. А поскольку ввод декодируется из stdin, ваши слушатели keydown получают обычные DOM-события.

С чего начать: демо с полосой установки

Канонический первый пример — карточка прогресса «Установка». Она использует setInterval, чтобы менять три спана, а TermDOM отрисовывает каждую мутацию:

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

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

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">Установка</div>
    <div>
      <span class="done" id="done"></span><span class="rest" id="rest"></span>
      <span class="pct" id="pct"></span>
    </div>
  </div>
`;

// TermDOM наблюдает за мутациями и перерисовывает экран сам.
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);

Полоса рисуется блочными символами, проценты выравниваются по правому краю через padStart, а цвета берутся из каскада CSS. Обратите внимание, чего здесь нет: никакого term.render(), ручного позиционирования курсора или склейки ANSI-строк. Обновление DOM и есть рендер.

Фреймворки

Поскольку документ и события следуют стандартам браузера, фронтенд-фреймворки работают почти без подготовки. В руководстве по началу работы глобальные объекты, которые ожидает каждый фреймворк, проверены на React 19 и Vue 3.5: React монтируется с одними document и window, а Vue дополнительно читает DOM-конструкторы для проверок instanceof и захватывает document при загрузке модуля — поэтому глобальные объекты фреймворка нужно установить до динамического импорта. Там же показано, что библиотека одинаково работает на Node, Bun и Deno: примеры импортируют @b9g/termdom по имени пакета.

Собираем небольшой TUI: интерактивный список задач

Флагманский пример проекта доказывает, как далеко заходит подход веб-платформы: официальный TodoMVC запускается с неизменённой логикой компонентов — поменяли только таблицу стилей. Та же разметка, тот же JavaScript, терминальная таблица стилей — и приложение становится TUI. Это сильнейший аргумент в пользу дизайна TermDOM: существующий веб-код можно перенацелить на терминал.

Вот минимальный интерактивный список с навигацией с клавиатуры на чистых DOM-API:

const {document} = term;
document.body.innerHTML = `
  <style>
    .list { border: 1px solid #888; width: 40ch; padding: 1ch; }
    .item { padding: 0 1ch; }
    .item.selected { background-color: #5fafff; color: #000; }
  </style>
  <div class="list" id="list"></div>
`;

const items = ["Собрать TUI", "Задеплоить", "Отпраздновать"];
const list = document.getElementById("list");
let selected = 0;

function render() {
  list.innerHTML = "";
  items.forEach((text, i) => {
    const div = document.createElement("div");
    div.className = "item" + (i === selected ? " selected" : "");
    div.textContent = text;
    list.appendChild(div);
  });
}

document.addEventListener("keydown", (ev) => {
  if (ev.key === "j") selected = (selected + 1) % items.length;
  if (ev.key === "k") selected = (selected - 1 + items.length) % items.length;
  render();
});

render();

События клавиатуры приходят как настоящие DOM-события на document, классы переключаются обычными строками, а выбранная строка получает цвет фона из CSS. Тот же код работает в браузере с другой таблицей стилей — в этом и смысл.

В каталоге примеров репозитория виден весь диапазон: просмотрщик Markdown с постраничной навигацией, когда документ выше терминала, стриминговый чат-клиент для LLM с историей и полем ввода, нечёткий поиск файлов, погодный виджет с эмодзи и flexbox-карточками дней, меню-бар из декларативных поповеров и полностью играбельный «Клондайк» с детерминированными раскладами.

Обзор возможностей

Прокрутка

Документы выше терминала прокручиваются через window.scrollTo() и element.scrollIntoView(). В примере файлового браузера в стиле NERDTree перемещение выделения вызывает scrollIntoView(), чтобы двигать «камеру»:

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

// Держим выбранную строку в зоне видимости.
rows()[selected].scrollIntoView();

События: клавиатура, мышь, фокус, вставка

События клавиатуры, мыши, фокуса и вставки срабатывают на элементах, документе и окне — они извлекаются из STDIN. Это одна из причин, почему фреймворки работают: они вешают слушателей на document и window ровно так же, как в браузере, а TermDOM предоставляет события. Поддержка мыши включает выделение перетаскиванием и кликабельные строки; вставка позволяет полю чата принимать текст из буфера обмена.

Формы: input, textarea, select, checkbox

<input>, <textarea>, <select>, чекбоксы и радиокнопки имеют поведение по умолчанию и «нативные» для терминала стили, которые можно менять обычным CSS. Работают навигация по Tab и стили :focus. Каретка — это настоящий курсор терминала, поэтому методы ввода CJK работают прямо в поле, с измерением в ячейках:

document.body.innerHTML = `
  <style>
    .field { margin-bottom: 1ch; }
    .label { color: #5fafff; }
    input { border: 1px solid #888; }
    input:focus { border-color: #5fafff; }
  </style>
  <div class="field">
    <div class="label">Имя</div><input id="name">
  </div>
  <div class="field">
    <div class="label">Приоритет</div>
    <select id="priority">
      <option>низкий</option><option>высокий</option>
    </select>
  </div>
`;

document.getElementById("name").addEventListener("input", updatePreview);

Веб-компоненты

customElements.define(), attachShadow(), <slot>, :host и изолированные стили работают как в браузере. Встроенные элементы форм сами являются теневыми деревьями. Значит, библиотеки компонентов, написанные для веба, а не для терминала, могут рендериться внутри TermDOM:

class StatusBadge extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({mode: "open"});
    shadow.innerHTML = `
      <style>
        :host { border: 1px solid #5fafff; padding: 0 1ch; }
        .ok { color: green; }
      </style>
      <span class="ok"><slot></slot></span>
    `;
  }
}
customElements.define("status-badge", StatusBadge);

document.body.innerHTML = `
  <status-badge>задеплоено</status-badge>
`;

Текст, выделение и полноэкранный режим

CJK, эмодзи и комбинируемые символы занимают правильную ширину; иврит и арабский рендерятся в визуальном порядке с контекстным формированием знаков, а каретка движется по графемам. Можно выделять текст перетаскиванием и стилизовать выделение через ::selection. Element.requestFullscreen() рисует элемент на альтернативном экране, а выход восстанавливает оболочку и её скроллбек — классическое поведение полноэкранных приложений.

TermDOM vs Ink vs blessed vs clack

Как выбрать? Таблица сравнивает четыре подхода, к которым чаще всего обращаются:

TermDOM Ink blessed clack
Модель Настоящий DOM + CSS React-компоненты Дерево виджетов Промпты
Раскладка Блочная модель, flexbox, таблицы Yoga (flexbox) Ручная absolute/grid Готовая
Стили Каскад CSS + наследование Инлайн-стили в JSX Объекты стилей на виджет Темы
Фреймворк Любой (vanilla, React, Vue…) Только React Нет Нет
Веб-компоненты Да (shadow DOM) Нет Нет Нет
Переиспользование веб-кода Да — замена таблицы стилей React-компоненты Нет Нет
Зрелость Молодой, пре-релиз Зрелый Зрелый (поддержка) Зрелый

Выбирайте TermDOM, когда хотите переиспользовать веб-навыки и веб-компоненты или когда ваша команда уже мыслит в HTML и CSS. Выбирайте Ink, если живёте в экосистеме React и нужна проверенная библиотека с компонентной моделью. Выбирайте blessed для классических терминальных приложений на виджетах, которым нужен тонкий контроль и не страшен низкоуровневый API. Выбирайте clack, когда нужен только набор аккуратных промптов для CLI.

Ограничения и оговорки ранней стадии

TermDOM молод. Пакет в npm — пре-релиз (0.1.3), в репозитории на момент написания статьи было около 133 звёзд, и API ещё может меняться. Проект честен в этом: матрица совместимости собирается набором проб, которые применяют каждый DOM-API, селектор и CSS-свойство к реальному документу и записывают, изменился ли вывод. Текущие цифры: 103 функции поддерживаются, 108 проверены и не работают, 301 CSS-свойство ещё не проверено.

Что это значит на практике:

В README также есть явный раздел «Что не поддерживается», так что вы встречаетесь с ограничениями до того, как встретите баг. Для экспериментов, внутренних инструментов разработчика и всего, где DOM браузерного уровня ценнее полировки виджетов, этот компромисс очень привлекателен.

Если сравнивать это с другими способами рендеринга HTML вне окна браузера, мой обзор HTML-in-Canvas API описывает противоположное направление: отрисовку настоящих DOM-элементов на canvas в пикселях, а не в ячейках.

FAQ

Что такое TermDOM?
TermDOM — это JavaScript-библиотека, которая отображает HTML и CSS в терминале. Она рисует реальные DOM-узлы в терминальном выводе и перерисовывает экран при их изменении, поэтому терминальные интерфейсы и интерактивные CLI можно писать на чистом JavaScript или с любым фронтенд-фреймворком. Установка: npm install @b9g/termdom, затем new TermDOM(), term.attach() — и document становится настоящим DOM-документом.
Заменяет ли TermDOM библиотеки Ink или blessed?
Это другая парадигма. Ink — библиотека на React, которая рендерит React-компоненты в строки терминала, blessed — классический набор виджетов, clack — библиотека промптов. TermDOM даёт настоящий DOM-документ с настоящим каскадом CSS: вы пишете веб-страницу и получаете TUI. Его стоит выбирать, когда важнее знакомые веб-технологии, CSS-раскладка и переиспользование веб-компонентов.
Можно ли использовать React или Vue с TermDOM?
Да. TermDOM работает с чистым JavaScript или любым фронтенд-фреймворком. React монтируется с одними document и window; Vue дополнительно читает DOM-конструкторы для проверок instanceof и захватывает document при загрузке модуля, поэтому глобальные объекты фреймворка нужно настроить до динамического импорта. Пример TodoMVC запускает официальную логику компонентов без изменений — поменяли только таблицу стилей.
Какие рантаймы поддерживает TermDOM?
TermDOM работает на Node.js, Bun и Deno. У библиотеки нет нативных компонентов, поэтому её можно скомпилировать в автономные бинарники с помощью таких инструментов, как bun build --compile. Примеры в репозитории запускаются на всех трёх рантаймах и импортируют пакет по имени.
Готов ли TermDOM к продакшену?
Проект молодой и распространяется как пре-релиз (0.1.3 на момент написания статьи). Матрица совместимости на termdom.org сообщает: 103 функции поддерживаются, 108 проверены и не работают, 301 CSS-свойство ещё не проверено. Для экспериментов и внутренних инструментов это отличный выбор, но перед запуском критичного продукта стоит оценить риски — API ещё стабилизируется.
Какой CSS поддерживает TermDOM?
Блочная модель, flexbox, табличная раскладка, цвета, фон, рамки, оформление и выравнивание текста работают в целых терминальных ячейках. CSS grid, переходы, анимации, opacity, transform и большинство логических свойств не поддерживаются. Физические свойства (border-top, margin-left) работают; логические (border-block, margin-inline) проверены и не дают эффекта.
В чём разница между TermDOM и DomTerm?
Эти два проекта — инверсия друг друга. DomTerm — это терминальный эмулятор Пер Ботнера, построенный из DOM-элементов: он помещает терминал в DOM. TermDOM помещает DOM в терминал: он рендерит DOM-узлы на экран терминала. Названия похожи до путаницы, поэтому направление стоит держать в голове.

Хотите создать терминальное или веб-приложение?

Терминальные интерфейсы на веб-стандартах — по-настоящему свежее направление, и TermDOM — самый буквальный его пример: пишете HTML и CSS, получаете TUI. Как full-stack разработчик, я создаю и консольные инструменты, и веб-приложения, и всегда выбираю ту модель рендеринга, которая делает продукт проще всего поддерживать.

Если у вас есть идея продукта — терминального или браузерного — и нужен взгляд со стороны на правильную архитектуру, напишите мне. Первая консультация бесплатная, без давления и навязывания.

Контакты

Давайте построим что-то быстрое

Есть проект? Помогу выбрать правильную архитектуру — веб-приложение, CLI-инструмент или что-то среднее — и сделать её правильно. Первая консультация бесплатно.