svg-terminal
v1.2.3
Published
Generate animated SVG terminals for GitHub READMEs from a declarative YAML config. 48 built-in blocks, 20 themes, zero runtime deps in the output.
Maintainers
Readme
svg-terminal
Generate animated SVG terminals from a declarative YAML config. The output is a single self-contained SVG that works inside GitHub's sandbox — no script, no external assets.
Demo above is the actual SVG this library produces. Source: examples/demo.yml. Regenerate with npm run demo.
Try in 60 seconds
npx svg-terminal init # writes terminal.yml
npx svg-terminal generate # writes terminal.svg
npx svg-terminal blocks # lists all 48 blocksOr as a GitHub Action — refresh your profile README on a schedule:
- uses: williamzujkowski/svg-terminal@v1
with:
config: terminal.yml
output: terminal.svg
commit: trueSee the full GitHub Action section below, the block catalog (48 blocks, one preview each), and the 20-theme gallery.
What's in the box
- Declarative YAML config — write blocks, pick a theme, run the CLI
- 48 built-in blocks — across identity, retro / fake-system, status, ASCII art, single- and multi-line animation, and humor categories. Browse the block catalog for previews of each
- 20 built-in themes — 12 classics (dracula, nord, monokai, the amber/green-phosphor/cyberpunk CRT trio, solarized-dark, win95, catppuccin, tokyo-night, gruvbox, high-contrast) plus 8 sharp, modern, WCAG-AAA OKLCH additions (modus-vivendi, oxocarbon, rose-pine, everforest, kanagawa, flexoki, github-light, dayfox)
- Frame animation —
BlockResult.animation = { frames, fps, loop }powers the 10 animated blocks (spinners, clock, dice, progress bar, etc.). Frames may be single- or multi-line as of #69 (jumping-jackis the reference multi-line block) - Dynamic-block cache — the 5 cacheable blocks (weather, github-stats, github-languages, quote, fun-fact) write to
.svg-terminal-cache.json. Pair with--frozen-cachefor offline CI builds - Reduced-motion respected —
@media (prefers-reduced-motion)clamps the CSS fade-ins AND (since v0.17) the frame cycle. SMIL-driven typing reveal, cursor walk, and scroll-on-overflow remain animated; pair with--staticfor full stillness - Schema-validated, XSS-safe — strict zod schema on every config field; user-controllable values are escaped at SVG emit sites. See SECURITY.md
- No runtime deps in the output — SMIL + CSS animation, inline, GitHub-sandbox-safe
- CLI + library —
npx svg-terminal generate, orimport { generate } from 'svg-terminal'. Requires Node 22+
Quick Start
npx svg-terminal init # Creates terminal.yml
npx svg-terminal generate # Generates terminal.svg
npx svg-terminal generate --watch # Rebuild on every save
npx svg-terminal blocks <name> # Inspect a block's config schemaCLI reference
Commands
| Command | Description |
|---------|-------------|
| generate | Generate the SVG from a config file |
| init | Create a starter terminal.yml (refuses to overwrite without --force) |
| themes | List available themes |
| blocks [<name>] | List block types, or print one block's config schema |
| cache check | Verify dynamic-block cache freshness (exit 1 on stale/missing) |
generate flags
| Flag | Description |
|------|-------------|
| --config <path> | Config file path (default terminal.yml) |
| --output <path> | Output file path (default terminal.svg) |
| --static | Non-animated final-frame snapshot |
| --minify | Strip inter-element whitespace |
| --strict | Promote soft warnings (unknown block-config keys, over-tall animated bands) to hard errors |
| --watch | Re-generate on config-file change |
| --timings | Print per-phase wall-clock timings to stderr |
| --explain | Print the resolved config + block list as JSON to stderr |
| --no-cache / --refresh-cache / --frozen-cache | Cache behavior shortcuts (off / re-fetch all / cached-only) |
| --cache-mode <m> | Explicit cache mode: normal | refresh | frozen | off |
| --version | Print the version |
Configuration
Edit terminal.yml:
theme: dracula
window:
title: "dev@my-machine:~"
terminal:
prompt: "dev@box:~$ "
blocks:
- block: neofetch
config:
username: dev
hostname: my-machine
role: Full-Stack Developer
languages: TypeScript, Rust, Go
- block: fortune
config:
fortunes:
- "The best code is no code at all."
- "Talk is cheap. Show me the code."
- block: custom
config:
command: echo "Hello!"
lines:
- "[[fg:green]]Welcome to my terminal![[/fg]]"svg-terminal init writes a fully-commented starter terminal.yml. The top-level keys:
| Key | Purpose |
|-----|---------|
| theme | Theme name, an inline theme object, or random (daily rotation) |
| blocks | The ordered list of blocks to render (each { block, config?, command?, color?, typing?, pause? }) |
| window | Chrome + frame: width, height/autoHeight (+ minHeight/maxHeight), title, style (macos|win95|floating|minimal|none), titleBarHeight, borderRadius |
| terminal | Text rendering: fontFamily, fontSize, lineHeight, prompt, padding/paddingTop |
| effects | textGlow, scanlines, vignette, shadow (booleans) |
| animation | Timing: loop, defaultTypingDuration, outputLineStagger, commandOutputPause, outputEndPause, defaultSequencePause, scrollDelay |
| chrome | Title-bar/button styling: titleFontFamily, titleFontSize, buttonRadius, buttonSpacing, dimOpacity |
| accessibility | { describe } — emit the <title>/<desc> screen-reader content (default true) |
| accessibilityLabel | Override the auto-generated aria-label |
| variables | Arbitrary values exposed to blocks (surfaced by --explain) |
| maxDuration | Hard cap (seconds) on the animation timeline (default 90) |
| scrollDuration | Per-scroll transition duration in ms (default 100) |
| fetchTimeout | Network timeout (ms) for dynamic blocks (default 10000) |
| cachePath / cacheTTL | On-disk cache file location + entry TTL for cacheable blocks |
Themes
| Theme | Description |
|-------|-------------|
| dracula | Dark purple/green theme (default) |
| nord | Arctic blue/frost palette |
| monokai | Classic warm dark theme |
| amber | Vintage amber CRT (pairs well with effects.textGlow: true) |
| green-phosphor | Classic green-on-black phosphor (pair with glow) |
| cyberpunk | Neon magenta/cyan on near-black |
| solarized-dark | Ethan Schoonover's solarized dark palette (lifted prompt/comment for WCAG AA) |
| win95 | Authentic Windows 95 chrome — auto-switches window.style: win95 |
| catppuccin | Catppuccin Mocha — soothing pastel dark theme |
| tokyo-night | Tokyo Night (storm variant) — popular for Vim/Neovim |
| gruvbox | Gruvbox Dark medium — retro warm contrast |
| high-contrast | WCAG AAA pure-black-on-white palette — accessibility / slides / projector |
OKLCH WCAG-AAA additions — sharp, modern, scanline-free; AAA body text (≥ 7:1), derived from the 400+ OKLCH scheme collection:
| Theme | Description |
|-------|-------------|
| modus-vivendi | Maximally-legible neutral dark (Protesilaos) — 21:1 body text |
| oxocarbon | IBM Carbon — cool high-contrast modern dark |
| rose-pine | Muted rosé & iris, soho dusk |
| everforest | Warm forest green, cozy low-glare |
| kanagawa | Hokusai ink-wash — indigo, sand, wave-crest |
| flexoki | Warm paper-and-ink reading palette (Steph Ango) |
| github-light | Clean professional light theme |
| dayfox | Soft warm-cream light theme |
Special value: theme: random rotates through all themes deterministically by day of year — gives you a different look every day without committing to one.
Each is the same 2-block config (motd + neofetch) rendered against the named theme. Source in examples/gallery/_template.yml.
| | | |---|---| | dracula | nord | | monokai | amber | | green-phosphor | cyberpunk | | solarized-dark | win95 | | catppuccin | tokyo-night | | gruvbox | high-contrast | | modus-vivendi | oxocarbon | | rose-pine | everforest | | kanagawa | flexoki | | github-light | dayfox |
Blocks
Run svg-terminal blocks to list all 48 (cacheable ones marked *), or svg-terminal blocks <name> to print one block's config schema directly without grepping the source.
| Block | Description |
|-------|-------------|
| neofetch | System-info display with configurable fields |
| fortune | Random quote/fortune in ASCII box |
| custom | Arbitrary text with [[fg:color]] markup |
| motd | Welcome banner / message of the day |
| dad-joke | Q&A joke in an ASCII box (daily rotation) |
| htop | Colorful process/resource monitor display |
| profile | Developer profile info card |
| goodbye | Farewell message with well-wishes |
| npm-install | Humorous npm dependency tree |
| blog-post | Blog post title in a box |
| national-day | Fun national day celebration |
| systemctl | Fake systemd service status |
| weather | Live weather from wttr.in (also embeds in MOTD) |
| github-stats | Live GitHub user stats (repos, followers) |
| github-languages | Top languages across a user's public repos, with percentage bars |
| quote | Random quote from dummyjson.com |
| fun-fact | Random fun fact from uselessfacts.jsph.pl |
| vim-exit | The eternal "how do I quit vim?" meme |
| sudo-sandwich | xkcd 149 callback |
| rm-rf | Dramatic fake rm -rf / with commentary |
| fork-bomb | Mock fork-bomb warning ("turn your laptop fan into a leaf blower") |
| kernel-panic | Friendly BSOD spoof in terminal text |
| segfault | Fake core dump with corrupted backtrace |
| whoami | Username + existential identity bullets |
| last-login | last output with awkward 3am login timestamps |
| finger | Faux finger(1) card with snarky plan lines |
| who | who listing with ghost users (debugger, coffee, sanity) |
| uptime | Ridiculous uptime ("up 632 days, that one incident") |
| matrix-rain | Single-frame Matrix rain screen with ACCESS GRANTED footer |
| cowsay | Speech bubble + ASCII cow (with word-wrap) |
| loading-spinner | Braille spinner cycling at configurable fps |
| heartbeat | Pulsing heart — emotional hook for a project you love |
| spinning-gear | Rotating \|/-\\ gear for DevOps/infra vibes |
| blinking-eyes | Kaomoji mascot that blinks every few seconds |
| countdown | T-minus N..0..GO! launch stinger (plays once, freezes on GO) |
| sparkline | ASCII sparkline (▁▂▄▇▆▅▃▂) from a numeric series |
| bbs-login | Retro 1980s BBS welcome banner — pairs with amber / green-phosphor |
| build-badge | Terminal-style project status card (tests / lint / coverage) |
| license-card | Boxed License / Copyright card |
| ascii-clock | HH:MM:SS clock with pulsing colon separators (12h / 24h) |
| progress-bar | Fake build progress bar that fills 0% → 100% |
| bouncing-dot | Single glyph bouncing left ↔ right |
| dice-roll | N d6 dice that tumble and land on a result |
| jumping-jack | Multi-line stick figure doing jumping jacks (reference multi-line animation) |
| palette-swatch | One-line render of all 16 theme palette colors |
| semver-bump | Current semver + bump preview (major/minor/patch) |
| ascii-calendar | Current-month calendar grid with today highlighted |
| toc | Auto-generated markdown anchor-link table of contents |
Dynamic API Blocks
Blocks marked with "Live" fetch data at build time from free, SFW APIs. They gracefully fall back to static content on failure.
# Weather in MOTD banner
- block: motd
config:
title: "MY TERMINAL"
weather:
location: NYC # City name or coordinates
units: imperial # imperial, metric, or both
# Standalone weather block
- block: weather
config:
location: "Los Angeles"
units: metric
compact: false
# GitHub profile stats
- block: github-stats
config:
username: your-github-username
# Top languages across a user's public repos
- block: github-languages
config:
username: your-github-username
top: 5 # top N languages (1-10, default 5)
barWidth: 20 # bar width in chars (5-40, default 20)
# Random fun fact
- block: fun-factSet fetchTimeout at the top level to control API timeout (default: 10000ms):
fetchTimeout: 15000 # 15 seconds — generous for slow APIsAccessibility
Every generated SVG carries role="img", an aria-label summary of the first commands, plus a <title> and <desc> as its first children. The <desc> contains the full final-frame content (every command prefixed with the prompt, every output line with color markup stripped) so screen-reader users can read more than the 5-command summary.
Opt out if your terminal output is sensitive and you don't want it duplicated as plain text inside the SVG payload:
accessibility:
describe: false # default true — emit <desc> with full contentReduced-motion caveat. The SVG emits an inline @media (prefers-reduced-motion: reduce) rule, which applies to CSS animations — the fade-ins and the frame cycle (single- and multi-line) honor it (migrated SMIL → CSS in v0.17). The remaining SMIL holdouts — typing reveal, cursor walk, and scroll-on-overflow — don't read the same CSS media query, so users who set the OS-level reduced-motion preference still see those animate. If that's a problem for your audience, generate with --static — same content, no motion at all.
Caching API responses
Dynamic blocks cache their responses in .svg-terminal-cache.json next to your config file (24h TTL by default). Commit that file alongside the YAML and CI builds become deterministic — no upstream hits, no diff churn from quote-of-the-minute drift.
# Top-level overrides
cacheTTL: 86400 # seconds (default 86400 = 24h)
cachePath: ".cache.json" # relative to the config file (must stay inside)CLI control:
svg-terminal generate # use cache if fresh, fetch + write back when stale
svg-terminal generate --refresh-cache # ignore cache entries, re-fetch everything
svg-terminal generate --frozen-cache # serve only cached values, never fetch (CI offline)
svg-terminal generate --no-cache # bypass cache entirely (don't read, don't write)Reproducibility note: committed cache + a generous cacheTTL makes CI reproducible; pair with --frozen-cache to make it truly offline (the build fails loudly if any block lacks a cached entry, rather than silently reaching for the network).
Privacy note: the cache file stores the raw API payloads. If a block fetches data you'd rather not commit (e.g. a private GitHub profile), either skip that block in versioned configs or add .svg-terminal-cache.json to .gitignore.
Custom Blocks
Custom blocks declare a strict zod configSchema so typos throw BlockConfigError at config-load time instead of silently falling back to defaults. Skipping the schema is allowed but discouraged — see CONTRIBUTING.md.
import { z } from 'zod';
import { registerBlock, generate } from 'svg-terminal';
registerBlock({
name: 'my-block',
configSchema: z.object({
greeting: z.string().optional(),
}).strict(),
render(context, config) {
const greeting = (config.greeting as string) ?? 'hello';
return {
command: 'my-command',
lines: [`[[fg:green]]${greeting}, world[[/fg]]`],
};
},
});Programmatic API
// ESM. Run as `node --experimental-vm-modules` or save as .mjs / set "type":"module".
import { generate, generateStatic } from 'svg-terminal';
async function main() {
const svg = await generate({
theme: 'nord',
blocks: [
{ block: 'neofetch', config: { username: 'dev' } },
{ block: 'custom', config: { command: 'date', lines: ['2026-05-25'] } },
]
}, {
// All fields optional. configPath anchors cachePath resolution if you
// want the cache; cacheMode is one of 'normal' | 'refresh' | 'frozen' | 'off';
// now lets you pin context.now for reproducible test/demo output.
configPath: '/abs/path/to/config.yml',
cacheMode: 'frozen',
now: new Date('2026-05-25T13:37:00Z'),
});
}
main();generateStatic returns the same content as a non-animated SVG — useful for accessibility fallbacks and social-preview cards.
inspectCache(userConfig, configPath) returns {filePath, results} where each result reports per-cacheable-block status (OK / STALE / MISS) with age in seconds — useful for building your own "is the cache hot?" CI gates without invoking the svg-terminal cache check subcommand. userConfig is the parsed config object; configPath anchors cache-path resolution.
GitHub Action
Complete .github/workflows/refresh-svg.yml for a weekly README refresh:
name: Refresh SVG terminal
on:
schedule:
- cron: '0 12 * * MON' # Mondays at 12:00 UTC
workflow_dispatch: # also run on demand
jobs:
refresh:
runs-on: ubuntu-latest
permissions:
contents: write # required for commit: true
steps:
- uses: actions/checkout@v4
- uses: williamzujkowski/svg-terminal@v1
with:
config: terminal.yml
output: terminal.svg
commit: trueFor maximally reproducible CI, commit .svg-terminal-cache.json alongside terminal.yml and set cache-mode: frozen — every build will serve cached payloads with zero network calls. The build fails loudly if a cacheable block is missing an entry:
- uses: williamzujkowski/svg-terminal@v1
with:
config: terminal.yml
output: terminal.svg
cache-mode: frozen # normal | refresh | frozen | off
static: false # set true to skip animation
minify: false # set true to strip inter-element whitespace
commit: true
commit-message: chore(readme): refresh terminal svgThe action commits as github-actions[bot]; the commit input only runs git add output + git commit + git push against the current branch. Skip commit: true and add your own commit step if you need signed commits or a custom author.
Inputs
| Input | Default | Description |
|-------|---------|-------------|
| config | terminal.yml | Path to the YAML config file |
| output | terminal.svg | Output SVG file path |
| cache-mode | normal | Dynamic-block cache behavior: normal | refresh | frozen | off |
| static | false | Generate a non-animated SVG (final-frame snapshot) |
| minify | false | Strip inter-element whitespace from the output |
| commit | false | Auto-commit the generated SVG (needs permissions: contents: write) |
| commit-message | chore: update terminal SVG [skip ci] | Commit message when commit: true |
Outputs
| Output | Description |
|--------|-------------|
| svg-path | Path to the generated SVG file (echoes output) |
| svg-bytes | Byte count of the generated SVG (integer string) |
| svg-size | Alias of svg-bytes — same value |
| svg-sha256 | Hex SHA-256 of the generated SVG, for cache-busting downstream |
| svg-changed | true | false — did this run modify a pre-existing SVG? false on the first run (no prior file) and when bytes are unchanged. Gate commits on this. |
- uses: williamzujkowski/svg-terminal@v1
id: term
with: { config: terminal.yml, output: terminal.svg }
- run: echo "size=${{ steps.term.outputs.svg-bytes }} changed=${{ steps.term.outputs.svg-changed }}"Text Markup
Blocks support inline color markup:
[[fg:green]]green text[[/fg]]
[[bg:blue]][[fg:white]]white on blue[[/fg]][[/bg]]
[[fg:cyan]][[bold]]bold cyan[[/bold]][[/fg]]
[[dim]]dimmed text[[/dim]]Tags: [[fg:color]] (foreground), [[bg:color]] (background), [[bold]], [[dim]] — each closed by its matching [[/fg]] / [[/bg]] / [[/bold]] / [[/dim]], and nestable.
Available colors: red, green, yellow, blue, magenta, cyan, white, orange, purple, pink, comment, plus bright_* variants. Colors resolve against the active theme palette; a raw hex value also works.
ASCII Boxes
The box generator supports multiple styles:
import { createBox } from 'svg-terminal';
createBox({ style: 'rounded', width: 40, lines: ['Hello!'] });
// ╭──────────────────────────────────────╮
// │ Hello! │
// ╰──────────────────────────────────────╯Styles: double (╔═╗), rounded (╭─╮), single (┌─┐), heavy (┏━┓), dashed (┌╌┐)
Contributing
See CONTRIBUTING.md for the local dev loop, the block/theme contribution recipes, and PR conventions.
License
MIT
