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
ITerminalOptionsfield 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_progressand more. - An imperative API:
XTermAPIturnswrite,clear,fit,find_next,serialize,connectand the rest into ordinary Reflex event handlers. - A reference PTY backend (
reflex_xtermjs.pty) that spawns a real pseudo-terminal, forwards resizes asTIOCSWINSZ, 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
tokenonly loopback clients are accepted, and even with a token remote clients needallow_remote=True. Browser connections are also checked against theirOrigin: only pages served from the same hostname as the backend, or listed inallowed_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 atokenthere. Run the backend as an unprivileged user, in a container, on a network you trust - or restrictcommandto 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-canvasstill 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-canvasto your own app's frontend dependencies if you want it.@xterm/addon-web-fontscurrently 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
-
Bump
versioninpyproject.tomland__version__incustom_components/reflex_xtermjs/__init__.py, and add a section toCHANGELOG.md. -
Merge
developintomainthrough a pull request. -
Tag
mainand 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.