one <script> · zero config · ~11 KB min+gzip

A terminal walkthrough
is just <x-zsh>

Write a terminal session in plain text. x-zsh renders an animated, Powerlevel10k-style zsh — typing, spinners, progress bars, the works — fully isolated in its own Shadow DOM. It's a walkthrough player, not an emulator.

note: Spin up the project locally — copy the commands with the button on each line. cmd: git clone https://github.com/acme/widget.git Cloning into 'widget'... spinner[2s]: Receiving objects => Received 2,418 objects, done. cmd: cd widget && npm install progress[2.4s]: Installing dependencies success: added 219 packages in 2s cmd: npm run dev info: ➜ Local: http://localhost:5173/ success: ✓ ready in 412 ms

Why x-zsh

Recording a terminal is fiddly and the output is a video you can't copy from. x-zsh is just markup — versionable, themeable, and the commands stay selectable.

✍️

Plain-text authoring

Keyword verbs and bare lines. No timelines, no JSON, no recording session.

🛡️

Shadow-DOM isolated

Its styles can't leak out and your page can't leak in. Drop it anywhere.

⌨️

Human-feeling typing

Jittered keystrokes, a blink at the prompt before each command, real pacing.

📊

Authentic effects

Spinners and five real progress-bar styles: pip, wget, curl, cargo, tqdm.

⏯️

Controls & loop

Optional play/pause, step, replay, and infinite looping with a delay.

🎨

Themeable

Every color is a CSS variable on :host. Light and dark built in.

Install

Pick a CDN tag or install from npm. The script auto-registers the <x-zsh> element on load — there's nothing to initialize.

CDN (fastest)

<!-- unpkg, latest (minified) -->
<script src="https://unpkg.com/x-zsh"></script>

<!-- or jsDelivr, pinned to a version -->
<script src="https://cdn.jsdelivr.net/npm/x-zsh@0.6/x-zsh.min.js"></script>

npm

npm install x-zsh
// side-effect import registers <x-zsh>
import 'x-zsh';

Then write a session

<x-zsh os="ubuntu" plugins="node">
  cmd: npm create vite@latest my-app
  success: ✔ Scaffolding project...
  cmd: cd my-app && npm install
  progress[2s]: Installing
</x-zsh>
Heads up: a standards custom-element name must contain a hyphen, so the tag is <x-zsh> (not <shell>). Register a different tag with XZsh.register('my-term') if you like.

Examples

Each terminal has a fixed height, so it scrolls internally and never makes the page jump. Scroll one into view to play it.

Python · pip progress · light mode

cmd: python -m venv .venv && source .venv/bin/activate cmd: pip install torch transformers progress[2.6s]: Downloading torch (797 MB) progress[1.6s]: Downloading transformers (9.8 MB) success: Successfully installed torch-2.3.0 transformers-4.41.0

Docker · spinners · looping with controls

cmd: docker compose up -d spinner[1.6s]: Creating network => Network app_default created spinner[2s]: Pulling images => Pulled 3 images success: ✔ Container app-web-1 Started cmd: docker compose ps info: app-web-1 running 0.0.0.0:8080->80/tcp

git workflow · error then recovery

cmd: git checkout -b feature/login info: Switched to a new branch 'feature/login' cmd: git push error: fatal: The current branch feature/login has no upstream branch. note: Set the upstream once, then it just works. cmd: git push --set-upstream origin feature/login spinner[1.4s]: Pushing => done success: branch 'feature/login' set up to track 'origin/feature/login'.

All five progress-bar styles

cmd: pip install package progress[1.8s pip]: Downloading (120 MB) cmd: wget https://example.com/image.iso progress[1.8s wget]: image.iso cmd: curl -O https://example.com/data.tar.gz progress[1.8s curl]: cmd: cargo build --release progress[1.8s arrow]: Compiling crate cmd: tar xzf archive.tar.gz progress[1.8s block]: Extracting

Rich prompt — Rocky Linux · Go · Terraform · Kubernetes · AWS

note: Ship the service to production. cmd: terraform apply -auto-approve spinner[2s]: Applying changes => Apply complete! 7 added, 2 changed. cmd: go build -o server ./cmd/server cmd: docker build -t registry/server:1.4.0 . progress[2s arrow]: Building image cmd: kubectl rollout restart deploy/server success: deployment.apps/server restarted info: rollout status: 3/3 pods ready

Pinned versions & a custom plugin

note: Pin any plugin with name@version — and register your own with XZsh.plugin(). cmd: node --version v22.3.0 cmd: acme deploy success: ✓ shipped via acme

Named themes — theme="dracula", theme="catppuccin", …

note: Set theme="dracula" — the palette changes, brand logos stay. cmd: npm test success: ✓ 128 passing warning: 1 deprecation
cmd: source .venv/bin/activate cmd: pytest -q success: ✓ 64 passed info: coverage 91%

Right-side tail — clock + last-command status

cmd: npm test error: 1 test failed cmd: npm run build success: built in 2.1s cmd: npm publish

Compact — icons only (compact)

cmd: docker compose up -d success: ✔ Started 3 containers cmd: kubectl get pods info: web-1 Running

User guide

The content between the tags is a tiny line-based language. Each line is an input the user types, an output the program prints, or a directive. Indentation is stripped, so you can nest it neatly in your HTML.

The one rule

A bare line is stdout of the command above it. You only reach for a verb when a line isn't plain output. Only the reserved words below (lowercase, followed by :) are treated as verbs — any other word: renders as ordinary output, so no escaping.

Input

VerbMeaning
cmd:The user types a command (prompt + typing animation).
root:A command needing root — rendered as sudo <command> on the normal prompt (won't double-prefix if you already wrote sudo).
type:The user types a response to a prompt; appends inline to the previous line (e.g. answering [Y/n]).
key:A keypress glyph — key: ^C, key: enter, key: tab.

Output

VerbMeaning
(bare line)Standard output.
warning:Yellow.
error:Red.
success:Green.
info:Cyan.
note:An author's aside, rendered as a dimmed shell comment (# text).

Time & effects

VerbMeaning
delay[800]:Pause; renders nothing.
spinner[2s]: label => doneSpinner for the duration, then ✓ done (the => done part is optional).
progress[3s]: labelProgress bar from 0→100% over the duration.

Screen & context

VerbMeaning
clear:Clear the screen.
prompt: dir=… branch=…Change prompt context mid-session — keys user host dir branch git.
# …A source comment; never renders.
Auto-tracked, no verb needed: cd app (including && chains) updates the directory segment; git init/git clone reveals the branch segment; git checkout -b dev switches the branch; source .venv/bin/activate (or conda activate) reveals the python venv segment.

Durations & styles

The bracket on timed verbs holds a duration and/or a style, in any order: progress[3s pip], spinner[1.5s line]. Durations accept 800 (ms), 800ms, 2s, 1.5s.

Progress stylesModeled on
block (default)tqdm / modern pip — 45%|████▌ |
pippip (rich) — ━━━━━╸━━━ 45%
wgetwget — 45% [====> ]
curlcurl — ###### 45.0%
arrowcargo / configure — [====> ] 45%

Spinner styles: dots (default), line, bar, arc, circle.

Attribute reference

All attributes are optional. Set them on the <x-zsh> tag.

AttributeDefaultWhat it does
osubuntuPrompt icon + brand color. One of ubuntu, debian, macos, arch, fedora, alpine, mint, manjaro, kali, centos, rhel, rocky, alma, opensuse, raspbian, gentoo, void, nixos, popos, elementary, windows, wsl, freebsd, termux.
modedarkdark or light.
themeNamed palette: tokyonight, dracula, nord, catppuccin, gruvbox, solarized, onedark, rosepine. Register more with XZsh.theme().
pluginsComma list of prompt segments. Languages/runtimes: node, python, docker, rust, go, ruby, php, java, kotlin, swift, deno, bun, dotnet, elixir, dart, zig, lua, perl, haskell. Infra/cloud: terraform, k8s, aws, gcp. python shows only after a venv is activated.
user host dir branchyou localhost ~ mainInitial prompt context.
titleuser@host: dirWindow title-bar text.
prompt-charThe prompt symbol before each command — e.g. $, %, #, , λ.
compactoffIcons-only prompt — hides the OS name and plugin versions (keeps dir/branch).
clockoffTicking time in the right prompt (tail). A red ✘ code status also appears there after a command whose output had an error:.
heightFixed screen height (bare number = px, or any CSS length). Scrolls internally, auto-scrolls to the latest line.
rowsFixed height in text rows (overrides height).
speed34Average ms per typed character.
gap900ms the prompt blinks before a command starts typing.
barblockDefault progress-bar style for the terminal.
controlsoffShow the step / play-pause / replay control bar.
loopoffReplay forever.
loop-delay1400ms to wait between loops.

It plays when scrolled into view and respects prefers-reduced-motion (renders instantly, no animation).

Dev guide

It's a single dependency-free file — a custom element, a line parser, and an async render loop. Hack on it in minutes.

Run the docs locally

git clone https://github.com/i-rocky/x-zsh
cd x-zsh
# any static server works
python3 -m http.server 8000
# open http://localhost:8000

How it works

Theming

Use a built-in palette with theme="…" (tokyonight, dracula, nord, catppuccin, gruvbox, solarized, onedark, rosepine), register your own, or override the CSS custom properties directly — they're all themeable:

// keys: bg fg muted accent accent2 ok warn err info comment dir git
XZsh.theme('mytheme', { bg: '#101418', fg: '#e6edf3', accent: '#3fb950' });
x-zsh::part(window){ border-radius: 6px; }
x-zsh{ --bg:#1e1e2e; --fg:#cdd6f4; --accent2:#89b4fa; }

OS/plugin segments keep their brand colors; the theme controls the background, text, prompt accent, output colors, and the dir/git segments.

Icons load on demand

Built-in logos are real brand icons vendored into the package (no runtime dependency on a third-party repo) and loaded lazily, tinted to the segment color via CSS mask — so the script stays ~11 KB and a logo is fetched only when used. The base resolves next to wherever you load x-zsh; override with XZsh.iconBase. Logos: Simple Icons (CC0) + a couple from devicon (MIT).

Versions & custom plugins

Pin any plugin's version inline with name@version, and register your own segments — point at a Simple Icons slug, any SVG url, or fall back to an emoji/text icon:

<x-zsh plugins="node@22.3.0, go@1.23, k8s@staging"> … </x-zsh>

// register before the element scrolls into view (e.g. in <head>)
XZsh.plugin('acme', { slug: 'docker', txt: 'v2', bg: '#5b21b6', fg: '#fff' });
XZsh.plugin('beta', { url: 'https://…/logo.svg', bg: '#222', fg: '#fff' });
XZsh.os('myos', { name: 'MyOS', slug: 'archlinux', bg: '#222', fg: '#fff' });

Custom tag

import 'x-zsh';
// also expose it under your own hyphenated name
XZsh.register('acme-term');