
Feature Gif Recorder
- 1 installs
- 23 repo stars
- Updated July 31, 2026
- ai-lab-yonder/ai-lab-agent-skills
feature-gif-recorder is a Claude Code skill that records a GIF of one webapp feature flow with the Playwright CLI, ready to embed in a PR description.
About
A Claude Code skill that drives a running webapp through one named feature flow with the Playwright CLI, captures frames, and stitches them into a labeled GIF ready to embed in a PR description. It emits an SHA-pinned <img> snippet for the PR body. A developer uses it when a shipped feature needs a visual demo or changelog GIF as PR evidence.
- Records a GIF of one webapp feature flow via the Playwright CLI
- Stitches frames into a labeled GIF with ffmpeg or ImageMagick
- Emits a ready-to-paste PR-embed <img> snippet
Feature Gif Recorder by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,750 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
feature-gif-recorder capabilities & compatibility
- Capabilities
- gif recording · browser automation · pr evidence · screenshot capture
- Works with
- playwright · github
- Use cases
- testing · documentation
- Pricing
- Free
What feature-gif-recorder says it does
Drive a running webapp through **one** named feature flow with the Playwright CLI (`npx playwright`), capture frames, stitch into a single labeled GIF, and emit a ready-to-paste PR-embed snippet.
Requires `ffmpeg` on PATH (preferred) **or** `magick` (ImageMagick). Refuse to record if neither is available
npx skills add https://github.com/ai-lab-yonder/ai-lab-agent-skills --skill feature-gif-recorderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 23 |
| Last updated | July 31, 2026 |
| Repository | ai-lab-yonder/ai-lab-agent-skills ↗ |
What it does
Record a Playwright-driven demo GIF of one webapp feature flow to embed in a PR description.
Who is it for?
Developers attaching a visual feature demo or changelog GIF to a pull request
Skip if: Recording multiple flows at once; one invocation equals one feature equals one GIF
When should I use this skill?
Shipping or testing a feature that needs a visual demo, changelog GIF, or PR evidence
What you get
A labeled feature GIF plus a ready-to-paste PR-embed snippet.
- feature demo GIF
- captured PNG frames
- PR-embed <img> snippet
By the numbers
- Runs 5 phases (Phase 0a through Phase 4)
- Defaults to 1280x800 viewport
Files
Feature GIF Recorder
Drive a running webapp through one named feature flow with the Playwright CLI (npx playwright), capture frames, stitch into a single labeled GIF, and emit a ready-to-paste PR-embed snippet. Single feature per invocation — no gallery sweep.
Constraints
- Requires the Playwright CLI on PATH (
npx playwright --version). If missing, instruct:npm i -D @playwright/test && npx playwright install chromium. Do not depend on the Playwright MCP server — talk to the CLI directly. - Requires
ffmpegon PATH (preferred) ormagick(ImageMagick). Refuse to record if neither is available — say so and stop. - One invocation = one feature = one GIF. Do not enumerate
docs/flows.mdand do not record multiple flows in a single run. - App must already be running and reachable at a URL the user supplies. Do not start servers.
- Output goes to
recordings/<feature-slug>/at the project root unless the user overrides. Non-destructive: append a timestamp suffix if the slug folder already has frames. - Never embed PII or production credentials in flows. If the spec hits an auth screen, ask for test creds — do not hard-code.
Phase 0a — PR Trigger
When the user is about to create a PR (running /commit-push-pr, gh pr create, or asking to "open a PR" / "make a PR"), the assistant MUST first ask:
"Record a demo GIF of the new feature for the PR description?"
- If yes → run this skill on the named feature, then embed the Phase 4
<img>snippet in the PR body before callinggh pr create. - If no → proceed without a GIF.
Do not skip the question. The user's answer may differ per PR.
Phase 0 — Detect & Confirm
1. Confirm the Playwright CLI is available: npx playwright --version. If not, tell the user to run npm i -D @playwright/test && npx playwright install chromium. 2. Confirm ffmpeg (or magick) is on PATH. 3. Identify the single target feature. In order of preference:
- User-named feature (slug, name, or "the X feature I just built") → use it.
- User-provided spec file path → read it; if multiple features, ask which one (do not loop over all).
- Otherwise: ask the user for app URL + the one feature (name, entry path, steps).
4. Read references/feature-spec.md for the spec schema this skill expects.
Phase 1 — Plan the Recording
For the chosen feature:
- Resolve a kebab-case slug (
add-todo,dark-mode-toggle). - Decide viewport (default
1280x800, override per-spec). - Decide frame cadence: capture before each user-visible state change plus a final frame. Timed sampling (e.g. every 250 ms) is allowed when the flow is animation-heavy — note this in the spec.
- Read
references/recording-strategy.mdfor when to snapshot vs. when to wait.
Print the plan (feature → step count → expected frame count) and ask the user to confirm before driving the browser. Skip confirmation only if the user passed --yes / explicitly authorized.
Phase 2 — Drive Playwright CLI
1. Generate a Playwright capture script at recordings/<slug>/capture.mjs that:
- Imports
chromiumfrom@playwright/test(orplaywright). - Sets the viewport to the spec's chosen size.
- Navigates to the entry URL.
- Takes screenshot
frames/000.pngafterload. - Walks the spec's steps using accessibility selectors (
page.getByRole,page.getByLabel,page.getByText), screenshottingframes/NNN.pngafter each user-visible state change.
2. Run it: npx playwright install chromium >/dev/null 2>&1 && node recordings/<slug>/capture.mjs. Use node, not playwright test, since these are scripted captures, not assertions. 3. Read references/playwright-cli.md for selector patterns, waiting strategies, and headless defaults. 4. On a step error, save what frames exist, mark the run partial, and stop. Do not retry forever.
Phase 3 — Stitch GIF
Run scripts/frames-to-gif.sh <slug>. The script handles palette generation and lanczos scaling for sharp output. Read references/gif-conversion.md for size/quality knobs (fps, width, dithering) and the ImageMagick fallback.
Output:
recordings/<slug>/
├── frames/000.png … NNN.png
├── <slug>.gif
└── meta.json # url, viewport, steps, timestamps, durationPhase 4 — PR Embed Snippet
After the GIF exists, the user must commit it on the branch they will PR from, then the skill prints a ready-to-paste HTML <img> snippet for the PR body.
Rule (private repos): anonymous https://raw.githubusercontent.com/... URLs 404 in PR/issue descriptions because they require a signed token query string the viewer does not have. Use a SHA-pinned github.com/.../blob/<sha>/<path>?raw=true URL inside an HTML <img> tag. Markdown  is also unreliable for private repos — HTML <img> goes through GitHub's authenticated session proxy.
Build the snippet:
1. Stage and commit the GIF on the feature branch: git add recordings/<slug>/<slug>.gif && git commit -m "docs: add <slug> demo gif". 2. Get the commit SHA: git rev-parse HEAD. 3. Determine <owner>/<repo> from the remote: gh repo view --json nameWithOwner -q .nameWithOwner. 4. Print:
<img src="https://github.com/<owner>/<repo>/blob/<sha>/recordings/<slug>/<slug>.gif?raw=true" alt="<feature-name> demo" width="720" />Use the SHA, not the branch name — branch names containing / (e.g. feat/dark-mode) get misparsed by GitHub's image proxy.
Last-resort fallback: if the HTML-tag form still does not render, drag-drop the GIF into the PR description in the GitHub web UI. That creates a https://github.com/user-attachments/assets/<uuid> URL that is publicly CDN-cached and always renders. There is no gh CLI / API path to do this upload programmatically.
Phase 5 — Summary
Print: feature name, slug, frames captured, GIF path, GIF byte size, status (ok / partial / failed), and the embed snippet from Phase 4. Suggest next step: paste the snippet into the PR body. Warn that GIFs can bloat the repo — .gitattributes LFS pattern is in references/gif-conversion.md.
Composition
- Pair with
e2e-runner/e2eskill if the user wants pass/fail assertions on top of visual evidence: this skill makes the artifact, that one makes the verdict.
Gotchas
Known failure points for feature-gif-recorder. Update whenever the skill produces incorrect output or hits a new edge case.
Format
Each gotcha follows this pattern:
- What goes wrong: description of the failure
- Why: root cause
- Fix: how to avoid or work around it
---
Playwright MCP Setup
_No gotchas yet._
Frame Capture Timing
_No gotchas yet._
GIF Conversion
_No gotchas yet._
Selectors & Waiting
_No gotchas yet._
Feature Spec Format
How to describe one feature flow so the skill can drive the Playwright CLI without ambiguity. One invocation = one feature = one GIF.
Two equivalent formats are accepted: a markdown block with key/value lines, or a small YAML/JSON object.
Markdown variant
# App URL
http://localhost:5173
# Feature: Add a todo
slug: add-todo
viewport: 1280x800
steps:
- goto /
- click role=button[name="New todo"]
- type role=textbox[name="Title"] "Buy milk"
- press Enter
- expect text "Buy milk" visible
notes: First-load empty state should be captured as frame 000.YAML variant
url: http://localhost:5173
slug: add-todo
viewport: { w: 1280, h: 800 }
steps:
- { action: goto, path: / }
- { action: click, selector: 'role=button[name="New todo"]' }
- { action: type, selector: 'role=textbox[name="Title"]', text: "Buy milk" }
- { action: press, key: Enter }
- { action: expect, kind: text, value: "Buy milk", state: visible }If the user points the skill at a file containing multiple feature blocks, the skill must ask which one to record — it does not loop.
Step verbs
| Verb | Meaning |
|---|---|
goto | Navigate to a path (relative to base URL) or absolute URL. |
click | Click a Playwright accessibility-tree selector (preferred over CSS). |
type | Focus an input + type text. |
press | Press a key (Enter, Escape, Tab, ArrowDown). |
hover | Hover an element; useful for menu reveals. |
scroll | Scroll a container or the page (scroll: down 400). |
wait | Sleep N milliseconds. Use sparingly — prefer expect. |
expect | Wait for a condition (text visible, role present, URL matches). |
Slug rules
- Lowercase kebab-case, no spaces.
- Becomes the folder name and the GIF filename.
Selector preference
Use Playwright accessibility locators (page.getByRole, page.getByText, page.getByLabel) over CSS. They survive markup churn.
GIF Conversion
How to turn frames/*.png into a small, sharp GIF.
Default: ffmpeg, two-pass with palette
The scripts/frames-to-gif.sh helper does this. Manual form:
slug=add-todo
src="recordings/$slug/frames"
out="recordings/$slug/$slug.gif"
fps=4
width=720
ffmpeg -y -framerate $fps -i "$src/%03d.png" \
-vf "scale=$width:-1:flags=lanczos,palettegen=stats_mode=diff" \
"$src/_palette.png"
ffmpeg -y -framerate $fps -i "$src/%03d.png" -i "$src/_palette.png" \
-lavfi "scale=$width:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
"$out"stats_mode=diff + paletteuse diff_mode=rectangle is the magic combo for UI screenshots — it keeps the palette stable between near-identical frames so flat backgrounds don't shimmer.
Knobs
| Knob | Effect | Default |
|---|---|---|
fps | Frame rate. Higher = smoother + bigger. | 4 |
width | Output width in pixels. Height auto-keeps ratio. | 720 |
dither=… | bayer:bayer_scale=5 for UIs, none for flat UI. | bayer 5 |
loop | -loop 0 infinite, -loop 1 play once. | 0 |
Size targets
- PR / changelog inline: ≤ 2 MB. Drop fps to 3 or width to 600 if you blow past this.
- Docs site hero: ≤ 5 MB.
- If the GIF is > 8 MB, switch to MP4 / WebM. MP4 in a
<video>tag is 5–10× smaller and loops fine; PRs and most markdown renderers accept it.
ffmpeg -y -framerate $fps -i "$src/%03d.png" -c:v libx264 -pix_fmt yuv420p \
-vf "scale=$width:-2:flags=lanczos,fps=$fps" "${out%.gif}.mp4"ImageMagick fallback
If ffmpeg is unavailable but magick is:
magick -delay 25 -loop 0 "$src/*.png" -layers Optimize -resize 720x "$out"Quality is worse — palette per-frame, no diff mode — but acceptable for short flows.
Repo bloat warning
Recorded GIFs add up fast. If you're committing them, add to .gitattributes:
recordings/**/*.gif filter=lfs diff=lfs merge=lfs -text
recordings/**/*.png filter=lfs diff=lfs merge=lfs -textOr .gitignore the frames/ folder and only commit the final GIFs.
Playwright CLI Reference
Notes on using the Playwright CLI (npx playwright) and the Node API to capture frames for GIF assembly. Full docs: <https://playwright.dev/>.
Install
npm i -D @playwright/test
npx playwright install chromium
# verify
npx playwright --versionIf the user is on a fresh machine, the first browser launch will trigger a Chromium download — npx playwright install chromium makes that explicit and one-time.
Capture script shape
For each feature, write recordings/<slug>/capture.mjs and run it with node:
import { chromium } from '@playwright/test';
import { mkdir } from 'node:fs/promises';
const out = (n) => `recordings/<slug>/frames/${String(n).padStart(3, '0')}.png`;
await mkdir('recordings/<slug>/frames', { recursive: true });
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await ctx.newPage();
await page.goto('http://localhost:5173/');
await page.screenshot({ path: out(0) });
await page.getByRole('button', { name: 'New todo' }).click();
await page.screenshot({ path: out(1) });
// ...one screenshot after each user-visible state change
await browser.close();Use node (not playwright test) — these are scripted captures, not assertions.
Selector preference
Prefer accessibility locators over CSS — they survive class-name churn:
| Locator | Use for |
|---|---|
page.getByRole('button', {name}) | Buttons, links, switches |
page.getByLabel(text) | Form inputs |
page.getByText(text) | Visible text |
page.getByTestId(id) | App-supplied test ids |
page.locator(css) | Last-resort CSS |
Frame numbering
Snap PNGs as zero-padded 3-digit names: 000.png, 001.png, … This keeps ls and ffmpeg's pattern matcher in lockstep:
ffmpeg -framerate 4 -i recordings/<slug>/frames/%03d.png ...If a flow exceeds 999 frames the cadence is wrong — increase the screenshot interval, do not jump to 4-digit padding.
Headless vs headed
Headless is the default and gives identical pixel output across machines, which is what you want for GIFs. Only switch to headless: false if the app refuses to render in headless (e.g. WebGL with webgl: 'fail').
Waiting strategies
Prefer await locator.waitFor({ state: 'visible' }) or await page.waitForLoadState('networkidle') over await page.waitForTimeout(N). Fixed sleeps make GIFs dance to your latency, not the app's.
Recovery
If a step throws mid-flow: 1. Catch the error in the capture script. 2. Snap one final frame (page.screenshot). 3. await browser.close(). 4. Exit with code 1 so the parent skill marks this feature partial. 5. Continue with the next feature. Do not retry forever.
Recording Strategy
When to snap a frame, how many, how fast.
Default cadence: state-change driven
Snap once after every user-visible change:
- After
gotoonce the page is loaded. - After every
click/press/typeonce the next state is visible (usebrowser_wait_foron the new text/role rather than a fixed sleep). - After every
expectthat succeeds.
This usually produces 5–15 frames per feature — small GIFs, sharp transitions.
Animation-heavy flows: timed sampling
If the feature includes a CSS transition, drag, or canvas animation that's the whole point of the demo, switch to timed sampling for that segment:
- Trigger the animation.
- Snap every 100–250 ms for the animation duration.
- Resume state-change cadence after.
Mark this in the spec with a cadence: timed 200ms for 2000ms directive on the step that triggers it.
Hover & focus
Browsers don't show hover/focus states in screenshots taken during navigation. To capture them: browser_hover (or browser_focus) → browser_wait_for time=200ms → snap frame.
Don't capture noise
Skip frames during:
- Page initial paint (snap once
loadfires, not before). - Font swap flashes — wait
document.fonts.readyif a custom font is in use. - Toast notifications you don't want in the demo — close them before snapping the next state.
Consistent pixel ratio
Always set browser_resize before navigation. Resizing mid-flow re-layouts and breaks the sequence. If both desktop and mobile flows are needed, run two separate features with different slugs.
Keep the flow under ~10 seconds of GIF
A 4 fps GIF with 30 frames is ~7.5 seconds — readers' attention span. If a flow needs more, split it into sub-features (checkout-step-1, checkout-step-2).
#!/usr/bin/env bash
# Stitch recordings/<slug>/frames/*.png into recordings/<slug>/<slug>.gif
# Usage: frames-to-gif.sh <slug> [fps] [width]
set -euo pipefail
slug="${1:?slug required}"
fps="${2:-4}"
width="${3:-720}"
base="recordings/$slug"
src="$base/frames"
out="$base/$slug.gif"
palette="$src/_palette.png"
if [[ ! -d "$src" ]]; then
echo "No frames at $src" >&2; exit 1
fi
if ! ls "$src"/*.png >/dev/null 2>&1; then
echo "No PNG frames in $src" >&2; exit 1
fi
if command -v ffmpeg >/dev/null 2>&1; then
ffmpeg -y -framerate "$fps" -i "$src/%03d.png" \
-vf "scale=${width}:-1:flags=lanczos,palettegen=stats_mode=diff" \
"$palette" >/dev/null 2>&1
ffmpeg -y -framerate "$fps" -i "$src/%03d.png" -i "$palette" \
-lavfi "scale=${width}:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \
"$out" >/dev/null 2>&1
elif command -v magick >/dev/null 2>&1; then
delay=$(( 100 / fps ))
magick -delay "$delay" -loop 0 "$src"/*.png -layers Optimize -resize "${width}x" "$out"
else
echo "Need ffmpeg or magick on PATH" >&2; exit 1
fi
bytes=$(wc -c < "$out" | tr -d ' ')
echo "GIF written: $out (${bytes} bytes)"
# App URL
{{APP_URL}}
# Feature: {{FEATURE_TITLE}}
slug: {{FEATURE_SLUG}}
viewport: 1280x800
steps:
- goto {{ENTRY_PATH}}
- {{STEP_1}}
- {{STEP_2}}
- expect {{ASSERTION}}
notes: {{NOTES}}
Related skills
FAQ
What dependencies are required?
The Playwright CLI on PATH plus ffmpeg (preferred) or ImageMagick's magick; it refuses to record if neither is available.
How many features per run?
One invocation equals one feature equals one GIF; it does not sweep multiple flows.