Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like 'make me a G Use when 需要Development领域自动化处理、数据分析和流程编排时使用。不适用于无明确需求的模糊场景。
Design & media
Demo GIF Skill
Try itRecord, render, optimize, and embed a demo GIF in a repo's README. Use when asked to add a demo gif, record a demo, show the tool in action, or make a README more visual.
What it does
Record, render, optimize, and embed a demo GIF in a repo's README. Use when asked to add a demo gif, record a demo, show the tool in action, or make a README more visual.
The skill document
demo-gif
Turns "add a demo GIF to this README" into a repeatable pipeline: pick the right recording method for the project, generate a reproducible recording script, render it, optimize the file size, and embed it in the README correctly.
Don't screen-record by hand and drop a 40 MB file in the repo. Every step here is scripted so the demo can be regenerated later when the tool changes.
Step 1 — Detect what kind of demo fits
Look at the repo before picking a method.
| Signal | Demo type | Method |
|---|---|---|
bin/, a CLI entry point, package.json with a bin field, argparse/click/cobra code, a Dockerfile that runs a command | CLI tool | Terminal recording (vhs) |
| A TUI framework (bubbletea, ratatui, textual, blessed, ink) | TUI | Terminal recording (vhs), larger window |
package.json with a frontend framework, a dev server script, index.html | Web app | Browser recording (Playwright + ffmpeg) |
| A library/SDK with no standalone entry point — README shows import + usage snippets | Library | REPL or a short example script, recorded as a terminal session |
If it's ambiguous, ask which surface the demo should show (the CLI, a specific screen, a code example) rather than guessing.
Step 2 — Generate the recording script
Terminal (CLI, TUI, library REPL) — preferred path: vhs
VHS by Charm renders a .tape file (a plain-text script of terminal actions) into a GIF/MP4/WebM deterministically — same input, same output, every time. That reproducibility is why it's the default here over screen-recording software.
Install:
- macOS:
brew install vhs - Windows:
scoop install vhs(pulls inttydandffmpegas dependencies automatically) orwinget install charmbracelet.vhs - Linux (Debian/Ubuntu): vhs isn't in the default apt repos — add charm's repo first, then install:
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg && echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list && sudo apt update && sudo apt install vhs ffmpeg. Installttydseparately from its GitHub releases — apt doesn't carry it. - Any platform with Go:
go install github.com/charmbracelet/vhs@latest - Docker:
docker run --rm -v $PWD:/vhs ghcr.io/charmbracelet/vhs .tape
Write a .tape file. Minimum viable structure:
Output demo.gif
Set Shell "bash"
Set FontSize 18
Set Width 1200
Set Height 600
Set Theme "Dracula"
Type "your-cli --help"
Enter
Sleep 2s
See references/tape-cookbook.md for the full command reference, typing-cadence guidance, and patterns for hiding setup steps. examples/cli-demo.tape is a complete runnable example.
Render with:
vhs demo.tape
Terminal fallback — no vhs available
If vhs can't be installed (locked-down CI box, unsupported OS), record with asciinema and convert with agg:
asciinema rec demo.cast
agg demo.cast demo.gif --theme monokai --speed 1.5 --idle-time-limit 2
asciinema rec captures a live session (less reproducible than a .tape script — there's no re-runnable source of truth), so prefer vhs whenever it's available. Full flag notes in references/tape-cookbook.md.
Web app — Playwright + ffmpeg
Playwright can record a real browser session to video via the recordVideo context option. Write a short script that opens the app, drives the interaction you want to show, and closes cleanly (the video only finalizes on context close).
const context = await browser.newContext({
recordVideo: { dir: 'recordings', size: { width: 1280, height: 720 } },
});
const page = await context.newPage();
await page.goto('http://localhost:3000');
// ...drive the demo interaction...
await context.close(); // required — video is written on close
See references/web-capture.md for the full setup (including page.video().saveAs() to get a predictable filename) and examples/web-demo.spec.ts for a complete script. Convert the resulting .webm/.mp4 to GIF with the ffmpeg two-pass palette flow in Step 4 — don't skip straight to a naive single-pass GIF encode, it produces banding on anything but flat-color UI.
Step 3 — Render
- Terminal:
vhs demo.tapeproduces the GIF (or MP4/WebM — set the extension in theOutputline) directly. - Web: run the Playwright script, then convert video to GIF (Step 4 covers the conversion, which doubles as the optimization pass).
Watch the raw output before optimizing. Re-record if:
- text is too small to read at the target embed width (see Step 5)
- the recording runs long — trim the script, don't just cut the GIF after the fact
- there's a visible mistake, a stray error, or dead time at the start/end
Step 4 — Optimize
Target: under 8 MB so it loads fast in a rendered README (GitHub hard-caps file display around 10 MB and truncates the render above that; 25 MB is the raw upload limit). Smaller is better for anyone on a slow connection.
From a video source (Playwright output) — ffmpeg two-pass palette:
ffmpeg -i input.mp4 -vf "fps=12,scale=800:-1:flags=lanczos,palettegen=max_colors=128" palette.png
ffmpeg -i input.mp4 -i palette.png -lavfi "fps=12,scale=800:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=sierra2_4a" output.gif
Two-pass beats a single-pass -vf ... format=gif encode because palettegen builds a palette tuned to the actual frames instead of using a fixed 256-color web-safe table — noticeably fewer banding artifacts on UI screenshots. Both filters and their options (max_colors, dither mode, etc.) come straight from ffmpeg -h filter=palettegen / filter=paletteuse — run those locally to confirm on your ffmpeg build if flags don't match.
If gifski is installed, it's a solid alternative for image-heavy web content: gifski -o output.gif --fps 12 --width 800 frame*.png (needs an image sequence, not a video file directly).
Already a GIF (vhs output, or after the ffmpeg pass above) — gifsicle:
gifsicle -O3 --lossy=30 --colors 128 output.gif -o output-optimized.gif
-O3runs gifsicle's most aggressive frame-diffing optimization (slower, smaller output than-O1/-O2).--lossy=Ntrades fidelity for size; default lossiness is 20,30-50is usually still clean for terminal/UI captures. Push higher only if size is still over budget.--colors 128caps the palette; drop to64for terminal recordings (few colors anyway) if still over budget.
Frame rate and dimensions (apply at the fps=/scale= step above, or via Set Framerate/Set Width in vhs):
- 10-15 fps is enough for a UI/terminal demo and cuts file size hard vs. 30 fps — motion isn't the point, showing the flow is.
- ~800px wide reads fine at README scale; render wider only if code/text needs to stay legible.
- 10-25 seconds total. Longer demos should be several short GIFs (one per feature) rather than one long one. Loop-friendly: end on a state that flows back into the start, so the loop doesn't jump.
Verify the result:
ls -la output-optimized.gif # confirm it's under 8 MB
If it's still too big: drop fps first, then width, then push --lossy higher, then cut duration. In that order — cutting duration first loses the most information for the least size gain.
Step 5 — Embed in the README
- Put the file at
docs/demo.gifor.github/demo.gif— commit it to the repo, don't point at an external host (imgur, a CDN, a gist). External hosts rot: links break, hotlinking gets blocked, and there's no guarantee the asset still matches the code it's demonstrating. - Plain markdown for a straightforward embed:
Write real alt text — what the GIF shows, not just "demo". - Use an `` tag instead when you need to control display width (GitHub renders markdown images at native size, which is often too wide):
- Dark/light: GIFs don't adapt to a viewer's color scheme, and GitHub's markdown renderer doesn't honor `` +
prefers-color-schemefor this — the mechanism GitHub actually documents is a URL fragment on each image:#gh-dark-mode-only/#gh-light-mode-only, e.g.next to. If the tool has both a dark and light terminal/UI theme and it matters, either pick the theme that reads better on a white README background (most repos are read on GitHub's light-mode-by-default web view) or commit two GIFs with the fragment suffixes above. Don't over-engineer this for a first pass — one well-chosen theme is usually enough. - Placement: near the top of the README, right after the title/one-line description and before installation instructions. That's the point where a reader decides whether to keep reading.
- Size sanity check before committing: re-run
ls -laon the final file and confirm it's under the 8 MB target from Step 4. Also check it renders — GitHub's README preview (or a local markdown preview) — before committing, since a corrupt or truncated GIF is a common two-pass-ffmpeg failure mode.
Common failure modes
- vhs renders a blank/black GIF: usually
ttydisn't installed or isn't on PATH — vhs shells out to it to actually run the terminal.vhs validate demo.tapeonly parses tape syntax — it does not checkttyd/ffmpegon PATH or verifyRequired binaries (confirmed against vhs 0.11.0: aRequired binary missing from PATH passesvalidateand only fails at render time). Check directly withttyd --versionandffmpeg -versionbefore rendering. - Playwright video is 0 bytes or missing: the context wasn't closed. Video only finalizes
await context.close(). - GIF looks banded/posterized: single-pass encode instead of the palettegen/paletteuse two-pass, or
--colorsset too low for a photo-real web capture (fine for terminal captures, bad for anything with gradients/photos). - File still too big after gifsicle: check duration and fps before pushing
--lossypast 60-80 — at that point you're better off cutting the recording shorter. invalid Set Theme "X": did you mean "Y": vhs theme names are case-sensitive and don't always match the common name (confirmed on vhs 0.11.0:Set Theme "Nord"fails, the real entry is lowercasenord;Set Theme "Monokai"fails too, since only variants likeMonokai Proexist).vhs validatedoesn't catch this — it only fails at render. Runvhs themes | grep -ito get the exact string before setting it.gifsicle/aggnot installed: this skill was hardened on a machine withvhs,ffmpeg, and Playwright but nogifsicleoragg. When gifsicle is missing, the fallback isn't a free win: adding anOutput demo.mp4line alongsideOutput demo.gifand running the ffmpeg two-pass palette flow (Step 4) on the.mp4is worth trying, but verify it's actually smaller before using it — on the short, mostly-static terminal recording used for this repo's owndocs/demo.gif, the mp4-round-trip result came out larger than vhs's direct GIF output (175KB vs. 126KB), because re-encoding through lossy video and rebuilding a palette from a video codec's compression artifacts loses more than it recovers for simple terminal captures. Compare both file sizes and keep the smaller one; don't assume the fallback helps.- A tape that runs
vhs(or any other tool not on the ambient shell's minimal PATH) inside the recorded terminal fails withcommand not found: vhs launches the shell it records with a minimal environment, not the caller's full PATH — confirmed on Windows/vhs 0.11.0, wherevhsitself (installed via winget, on PATH in the invoking terminal) is not reachable from inside a tape's ownType "vhs ..."command. If a demo needs to show a tool running, confirm it's reachable inside the recorded shell first (e.g.Type "which "as a throwaway check) rather than assuming the outer environment carries in. - Nesting a live
vhs .taperender inside another vhs recording crashes withpanic: write tcp ...: use of closed network connection: this is a go-rod/chromedp browser-automation collision between the inner and outer vhs processes, confirmed reproducible on vhs 0.11.0. Don't record a tape whose captured commands include a realvhsrender — pre-render the inner GIF as a separate, non-nested step (before the outer recording starts) and reference its real output (e.g. viacat/ls) in the tape that's actually being recorded.
Related skills
测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试
Turn vague or incomplete product ideas into polished, runnable demos with minimal user prompting. Use when a coding agent is asked to create, plan, prototype...
Use when converting a video clip into a GIF with ffmpeg. Supports trimming by start time and duration, controlling frame rate and width, and returning the ou...
Demo skill that turns a raw digital-content file (e.g. a markdown note) into a reusable skill package. Given a source file, it builds a SKILL.md, extracts me...
Reviews a short product idea and returns structured feedback. Use when the user asks for a lightweight product idea critique.