Skip to main content

reflex-xtermjs

xterm.js - the terminal that powers VS Code, Hyper and Azure Cloud Shell - as a Reflex custom component, with the official addons, twelve built-in themes, an imperative API you drive from Python, and an optional PTY backend that gives the browser a real shell.

pip install reflex-xtermjs

Requires Python 3.11+ and Reflex 0.10 or newer (tested on Python 3.11-3.14 with Reflex 0.10.0). Reflex 0.9.x users should stay on reflex-xtermjs 0.1.x.

What you get

  • The full terminal. Every ITerminalOptions field from xterm.js 6 is a typed Reflex prop, applied in place so changing a font or a theme never destroys the buffer.
  • The official addons - fit, search, web-links, serialize, clipboard, unicode11, unicode-graphemes, image (SIXEL / iTerm / Kitty), ligatures and progress - selected with a single addons=[...] list.
  • DOM, WebGL and canvas renderers, switchable at runtime.
  • Events into Python: on_data, on_key, on_resize, on_title_change, on_selection_change, on_search_results, on_progress and more.
  • An imperative API: XTermAPI turns write, clear, fit, find_next, serialize, connect and the rest into ordinary Reflex event handlers.
  • A reference PTY backend (reflex_xtermjs.pty) that spawns a real pseudo-terminal, forwards resizes as TIOCSWINSZ, and is locked down to localhost with a token by default.
  • Twelve themes out of the box: VS Code Dark+, Dracula, Nord, Solarized (dark and light), Gruvbox Dark, One Dark, Tokyo Night, Catppuccin Mocha, Monokai, GitHub Dark and GitHub Light.

Quick start

import reflex as rx
from reflex_xtermjs import THEMES, XTermAPI, xterm

term = XTermAPI("hello")


class State(rx.State):
    @rx.event
    def greet(self):
        return term.writeln("\x1b[1;32mhello from the backend\x1b[0m")

    @rx.event
    def echo(self, data: str):
        """Every keystroke arrives here."""
        return term.write(data)


def index() -> rx.Component:
    return rx.vstack(
        rx.button("Greet", on_click=State.greet),
        rx.box(
            xterm(
                terminal_id="hello",
                theme=THEMES["dracula"],
                addons=["fit", "web-links", "search"],
                renderer="webgl",
                font_size=14,
                cursor_blink=True,
                on_data=State.echo,
            ),
            height="400px",
            width="100%",
        ),
    )


app = rx.App()
app.add_page(index)

The terminal fills its parent, so give the wrapping box a height.

The component

xterm(...) accepts the props below. Python snake_case names are emitted as the camelCase names xterm.js expects.

Identity and layout

Prop Type Default Meaning
terminal_id str "default" Key the imperative API uses. Unique per page.
auto_fit bool True Resize to the container through the fit addon.
fit_debounce_ms int 60 Debounce on container resize events.
cols / rows int - Explicit geometry; used when auto_fit is off.
width / height str "100%" CSS size of the terminal container.

Addons and rendering

Prop Type Default Meaning
addons list[str] ["fit", "web-links"] Any of fit, search, web-links, serialize, clipboard, unicode11, unicode-graphemes, image, ligatures, progress.
renderer "dom" | "webgl" | "canvas" "dom" Renderer backend.
unicode_version str addon default Unicode version activated by the unicode addons.
image_options ImageOptions - SIXEL / iTerm / Kitty limits.
ligature_options LigatureOptions - Ligature fallbacks and font features.
search_options SearchOptions - Defaults merged into every search.

An unknown addon name raises ValueError while the page is being built, rather than failing silently in the browser. Addons that use xterm.js' proposed API (unicode11, unicode-graphemes, ligatures, image, progress, serialize) turn allow_proposed_api on automatically unless you set it yourself.

Terminal options

All of them are optional and fall back to the xterm.js default: allow_proposed_api, allow_transparency, alt_click_moves_cursor, convert_eol, cursor_blink, cursor_style, cursor_inactive_style, cursor_width, custom_glyphs, disable_stdin, draw_bold_text_in_bright_colors, fast_scroll_modifier, fast_scroll_sensitivity, font_family, font_size, font_weight, font_weight_bold, ignore_bracketed_paste_mode, letter_spacing, line_height, log_level, mac_option_click_forces_selection, mac_option_is_meta, minimum_contrast_ratio, overview_ruler, reflow_cursor_line, rescale_overlapping_glyphs, right_click_selects_word, screen_reader_mode, scroll_on_erase_in_display, scroll_on_user_input, scroll_sensitivity, scrollback, smooth_scroll_duration, tab_stop_width, theme, window_options, windows_pty, word_separator.

Plus two conveniences: initial_text (written once on mount) and read_only (a friendlier spelling of disable_stdin).

Events

Event Handler signature
on_data (data: str)
on_binary (data: str)
on_key (key: str, event: dict)
on_title_change (title: str)
on_resize (cols: int, rows: int)
on_ready (cols: int, rows: int)
on_bell, on_line_feed, on_cursor_move, on_write_parsed, on_context_loss ()
on_scroll (position: int)
on_selection_change (selection: str)
on_render (start: int, end: int)
on_search_results (result_index: int, result_count: int)
on_progress (state: int, value: int)
on_link_click (uri: str)
on_socket_open (url: str)
on_socket_close (code: int, reason: str)
on_socket_error (message: str)

Driving the terminal from Python

The React shim registers each mounted terminal on window.__reflexXterm[terminal_id]. XTermAPI wraps that registry in event specs, so you never re-render the component to change what it shows.

from reflex_xtermjs import XTermAPI

term = XTermAPI("hello")

rx.button("Clear", on_click=term.clear())
rx.button("Fit", on_click=term.fit())
rx.button("Find", on_click=term.find_next("ERROR", {"caseSensitive": True}))

Output, input and focus: write, writeln, clear, reset, refresh, input, paste, focus, blur. Geometry: fit, propose_dimensions, resize, get_size. Options: set_option, set_options, get_option, set_renderer, load_addons, clear_texture_atlas. Selection: select_all, select, clear_selection, get_selection. Scrolling: scroll_to_top, scroll_to_bottom, scroll_to_line, scroll_lines, scroll_pages. Search: find_next, find_previous, clear_search_decorations. Serialize: serialize, serialize_as_html. Socket: connect, disconnect, send, is_connected. Anything else: term.call("someMethod", arg1, arg2).

Calls that produce a value take a callback:

class State(rx.State):
    transcript: str = ""

    @rx.event
    def save(self, value: str):
        self.transcript = value


rx.button("Save transcript", on_click=term.serialize(callback=State.save))

Themes

from reflex_xtermjs import THEMES, THEME_NAMES, XTermTheme, get_theme

xterm(theme=THEMES["nord"])
xterm(theme=get_theme("gruvbox-dark"))

# start from a built-in theme and override a colour
mine = XTermTheme(**{**THEMES["dracula"].dict(), "background": "#1a1a24"})

A theme is an XTermTheme, which mirrors xterm.js' ITheme: background, foreground, cursor, cursor_accent, selection_background, the eight ANSI colours, their eight bright variants, the scrollbar slider colours and extended_ansi for palette entries 16-255. Unset colours are omitted rather than sent as null, so xterm.js keeps its own defaults. Plain dicts work wherever a theme is accepted, which is what you want for a computed var.

The PTY backend

reflex_xtermjs.pty spawns a real pseudo-terminal and pipes it to the browser through @xterm/addon-attach.

import reflex as rx
from reflex_xtermjs import xterm
from reflex_xtermjs.pty import PtyConfig, pty_app

app = rx.App(
    api_transformer=pty_app(PtyConfig(command="/bin/bash", token="a-secret")),
)
xterm(
    terminal_id="shell",
    websocket_url="ws://localhost:8000/pty?token=a-secret",
    addons=["fit", "web-links", "clipboard"],
)

PtyConfig takes command, args, cwd, env, term, cols, rows, path, token, max_sessions, allow_remote and allowed_origins.

On the wire it is the plain addon-attach protocol - raw bytes both ways - plus one control frame the component sends whenever the terminal is resized:

{"type": "resize", "cols": 120, "rows": 30}

The backend only treats a text frame as control if it parses as JSON and carries a known type; everything else goes to the shell verbatim. Set send_resize_on_socket=False on the component if your own backend does not want those frames.

Security. A PTY endpoint hands whoever reaches it a shell with the backend's privileges, with no sandbox. The defaults are deliberately tight: without a token only loopback clients are accepted, and even with a token remote clients need allow_remote=True. Browser connections are also checked against their Origin: only pages served from the same hostname as the backend, or listed in allowed_origins, may open a shell - this blocks cross-site WebSocket hijacking of a localhost terminal. Behind a reverse proxy every client looks local, so always set a token there. Run the backend as an unprivileged user, in a container, on a network you trust - or restrict command to a single program instead of a login shell.

During development Reflex serves the frontend and the backend on different ports, so a bare /pty path would hit the Vite dev server. Build the URL from the configured backend API URL; the demo app's pty_config.py shows one way.

Running the demo

git clone https://github.com/ecrespo/reflex-xtermjs
cd reflex-xtermjs
uv venv && uv pip install -e . && uv pip install -r xtermjs_demo/requirements.txt
cd xtermjs_demo && uv run reflex run

Four pages: a live PTY shell, a playground where every option is a control, a terminal driven entirely from Reflex State (a small shell implemented in Python), and the addon surface - search, serialize, web-links and progress.

npm dependencies

The component pins the packages it imports:

@xterm/xterm@6.0.0            @xterm/addon-clipboard@0.2.0
@xterm/addon-fit@0.11.0       @xterm/addon-webgl@0.19.0
@xterm/addon-search@0.16.0    @xterm/addon-unicode11@0.9.0
@xterm/addon-web-links@0.12.0 @xterm/addon-unicode-graphemes@0.4.0
@xterm/addon-serialize@0.14.0 @xterm/addon-image@0.9.0
@xterm/addon-attach@0.12.0    @xterm/addon-ligatures@0.10.0
                              @xterm/addon-progress@0.2.0

Two official addons are deliberately left out of that set:

  • @xterm/addon-canvas still declares a peer dependency on xterm 5.x, so installing it alongside xterm 6 breaks dependency resolution for everyone. renderer="canvas" loads it lazily and falls back to the DOM renderer with a console warning when it is absent. Add @xterm/addon-canvas to your own app's frontend dependencies if you want it.
  • @xterm/addon-web-fonts currently requires an xterm 6.1 beta.

Development

uv pip install -e ".[dev]"
uv run pytest                 # unit tests
uvx ruff check . && uvx ruff format --check .
uv build                      # sdist and wheel in dist/

Reflex 0.10 removed the reflex component CLI. After changing a prop or an event trigger in xtermjs.py, regenerate the type stub with Reflex's stub generator, then format it:

PYTHONPATH=. uv run python -m reflex_base.utils.pyi_generator custom_components/reflex_xtermjs
uvx ruff format custom_components/reflex_xtermjs/xtermjs.pyi

CI (.github/workflows/quality.yml) runs ruff, pytest on Python 3.11-3.14, a production build of the demo app on Node 24, and a wheel/sdist check; security.yml runs bandit, pip-audit, gitleaks, CodeQL and dependency review.

Releasing

  1. Bump version in pyproject.toml and __version__ in custom_components/reflex_xtermjs/__init__.py, and add a section to CHANGELOG.md.

  2. Merge develop into main through a pull request.

  3. Tag main and push the tag:

    git checkout main && git pull
    git tag -a v0.2.0 -m "v0.2.0" && git push origin v0.2.0
    

The Release workflow re-runs the quality gate, checks that the tag matches the package version, publishes to PyPI through Trusted Publishing and creates the GitHub release with the changelog notes.

License

Apache-2.0. xterm.js itself is MIT-licensed by the xterm.js authors.

Metadata

Release files for reflex-xtermjs 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reflex-xtermjs 0.2.0
File Size Uploaded Provenance Actions
reflex_xtermjs-0.2.0.tar.gz 40.1 kB With provenance Details for reflex_xtermjs-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for reflex-xtermjs 0.2.0
File Interpreter ABI Platform Actions
reflex_xtermjs-0.2.0-py3-none-any.whl Python 3 none any Details for reflex_xtermjs-0.2.0-py3-none-any.whl

Total release size: 75.0 kB

Release files / reflex_xtermjs-0.2.0.tar.gz

Download URL reflex_xtermjs-0.2.0.tar.gz
Size 40.1 kB
Tags Source
SHA-256 checksum
How to use checksums
8103ede449419d45cb3a8766a57bd165ba0f04d0e46ec299dfd1fa81705087b9
BLAKE2b-256 checksum
How to use checksums
fd862c52e671ec276e97b91b45677764454b22b9e1e7754ae871d4c5d7392bfc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / reflex_xtermjs-0.2.0-py3-none-any.whl

Download URL reflex_xtermjs-0.2.0-py3-none-any.whl
Size 35.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7451be7da89d8461943333faee5a667ebcec578519a0b8b2ad1fb70a820faace
BLAKE2b-256 checksum
How to use checksums
4bafa0907f3aa6d28c0a4e5ff7f1b29f4c90768ea4cb0b5801744f328b8cdf0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Security for reflex-xtermjs 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for reflex_xtermjs-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page