About ui-recording-timeline
UI recording timeline is a skill that turns interface recordings into self-contained webpages with an annotated timeline, for examining page loads and UI changes. It combines frame review, change-region analysis, and video overlays in a portable output folder.
Key features
- 01Extract recording frames and metadata
- 02Find candidate interface changes
- 03Define phases, metrics, and paint events
- 04Compute and inspect change regions
- 05Build an interactive timeline page
Use cases
- 01Document page-load phases and timing
- 02Analyze interface updates and layout shifts
- 03Mark blank waits and the final idle phase
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ rauchg/ skills --skill ui-recording-timelineAfter installing, type/ui-recording-timelinein Claude Code to confirm it loaded.
Bundled filesassets/template.htmlscripts/timeline.py
Details
From the project READMEUI recording timeline
Produces a self-contained folder with index.html and the video. The video fills the window. Below it are a one-line caption and a thin timeline (phase colors, paint ticks, hover thumbnails, scrubbing). Change regions flash on the video at each paint, and a paint list drawer opens with l.
Everything needed is in this skill directory:
scripts/timeline.py: one CLI with all the steps. Dependencies are declared inline, anduv runinstalls them.assets/template.html: the page.buildinjects the data into it.
Requirements: ffmpeg/ffprobe and uv on PATH. Chrome or Chromium is needed only for preview.
Below, T means uv run <this skill dir>/scripts/timeline.py. Define it as a shell function, not a variable: zsh doesn't word-split variables. Use the absolute path of the skill directory:
T(){ uv run "/abs/path/to/ui-recording-timeline/scripts/timeline.py" "$@"; }
WD is the output folder. If the user's project has conventions for where research or collateral goes, follow them; otherwise put WD next to the video as <video-stem>-timeline/.
Workflow
1. Extract and find candidate paints
T extract /path/to/recording.mp4 WD # copies video, writes meta.json (frame timestamps; VFR is fine)
T candidates WD # table of frames that differ from the previous frame
Each row has the frame index, time, % changed, kind (change / full / cursor?), and the normalized bbox. Small cursor-like changes are hidden (use --all to show them).
2. Look at the frames, then decide what counts as a paint
Never label from the table alone. Look at the frames:
T sheet WD --frames 0,4,14,26,61 --name overview # labelled contact sheets -> WD/sheets/*.jpg
T sheet WD --frames 18,19 --crop 0,0,0.25,0.15 --name zoom # zoom into a small change
Read the sheet images with the image viewer. Make sure to include the frame before each candidate, so you can see what changed.
Rules for choosing paints:
- Is a paint: a visible content or layout change. That covers skeletons appearing, real content replacing skeletons, navigation to a blank page, a new page's first paint, a label or button-state change, and browser chrome that signals load state (the reload icon replacing the stop X).
- Is not a paint: cursor movement, hover highlights, spinner rotation, caret blink, or a gradual shimmer on skeletons that are already there. Look at the bbox: tiny regions that wander across consecutive frames are the cursor.
- Animations: a multi-frame animation (a check-mark animating, a panel sliding in) is one paint at its final frame. Give it
"base": <frame before the animation started>so the diff compares the whole animation. - Minor paints: set
minor: trueon small in-place updates (a label, a button state, browser chrome). They get shorter ticks and muted text. - Phases: group paints into phases by screen or URL (for example: app shell, SSO gate, IdP, blank redirect, signed-in app, idle). The address bar in the frames usually tells you. Phases must cover 0 to the recording's duration with no gaps.
- Idle phases: set
"idle": trueon stretches where nothing on screen changes. That always includes the tail after the last paint, and can include long waits on one unchanged screen. Idle phases are drawn hatched grey, not in a palette color: a saturated color would suggest activity, and dark grey makes the bar look like it ends early. Don't trim the idle tail; it's real time, and the video spans it. - Metrics: the first two appear in the caption, so put the most important first (for example "main content" and "visually complete"). Others, like time on the IdP or total blank time, go in the drawer.
- Caveat: t=0 is the first frame of the recording, not navigation start. If the first frame already shows UI, say so in
notes.
3. Write WD/paints.json
{
"title": "vercel.com/vercel-labs with an SSO redirect",
"notes": "Recording starts mid-navigation: the dashboard shell is already on screen at t=0.",
"metrics": [
{"value": "6.50 s", "label": "main content"},
{"value": "8.37 s", "label": "visually complete"},
{"value": "2.53 s", "label": "on Okta"}
],
"phases": [
{"start": 0.0, "end": 0.802, "name": "Dashboard (unauthenticated)"},
{"start": 0.802, "end": 2.002, "name": "Vercel SSO gate"},
{"start": 2.002, "end": 8.468, "name": "Okta and back"},
{"start": 8.468, "end": 10.935, "name": "Idle", "idle": true}
],
"paints": [
{"frame": 0, "label": "Dashboard shell: sidebar nav and header skeleton (recording start)"},
{"frame": 4, "label": "Projects and usage skeleton cards"},
{"frame": 19, "label": "Team switcher changes to \"Select Team\"", "minor": true},
{"frame": 87, "label": "Okta Verify check badge (animates 2.668 to 3.068 s)", "minor": true, "base": 74}
]
}
frameis the decoded frame index fromcandidates. The time comes from the frame's real timestamp, so don't hand-compute it. Phase boundaries are in seconds: use thetof the paint that starts each phase, and the duration fromextractfor the last one.- Labels are short and concrete. Say what appeared, changed or disappeared, and quote on-screen text.
- Optional per phase:
"color": "#hex". By default colors come from a palette.
4. Compute and check the change regions
T regions WD # move-aware diff for every paint -> regions.json
T check WD # boxes drawn on every paint frame -> WD/sheets/check-*.jpg
Look at every check sheet. Pink means painted, blue means moved, and thin grey shows where the content moved from. How the diff works (details in compute_regions):
- Changed layout shapes are snapped to whole elements: words, icons, inputs, cards. Faint shimmer ends are included.
- A shape found nearby in the previous frame, whose old spot was vacated, counts as moved. For example, when a sidebar item is inserted, the rows below it slide down. Same-offset moves are grouped.
- Ambiguous repeated shapes (identical chevrons, rows) count as moved when they line up with an offset already found in the same frame.
- The cursor is learned, not guessed from size. Cursor sprites (arrow, hand, text cursor) come from the
candidatesframes where only something small moved on its own. Only boxes containing a known sprite, in either frame, are dropped. Small real paints like a logo, an icon or a toolbar button are kept. So runcandidatesbeforeregions. - Everything else is painted. That includes flat fills like a new backdrop.
If a box looks wrong:
- First check the paint choice. A wrong
baseor a hover state inside the paint frame is the usual cause. - Only then consider tuning the constants at the top of
compute_regions. They're tuned at 1710 px wide and scaled automatically:EDGE/CHGthresholds,SEARCHdistance, the cursor size, and the grow tolerance.
5. Build, preview, and open
T build WD # paint thumbnails + WD/index.html
T preview WD --t 6.502 # headless screenshot with boxes frozen -> WD/preview.png
open WD/index.html
Read preview.png to verify the layout and that the overlay lines up with the video. Re-run build after any change to paints.json or regions.json. Run regions again only when the paint frames or bases change.
Page reference
- Keys:
←/→step between paints,spaceplays,dtoggles the change flashes,lopens the paint list drawer,fgoes fullscreen. Clicking the video plays or pauses. Clicking or dragging the timeline scrubs, and hovering shows a thumbnail. - URL options:
#t=6.502deep-links to a time, and?holdfreezes the boxes, which helps with screenshots. - "Shown" is the time until the next paint.
- The folder is portable:
index.html,recording.*,paints/.meta.json,paints.json,regions.jsonandsheets/are inputs and debug output, safe to keep.
Reporting back
Summarize the phases, the key metrics, and anything notable (blank screens, layout shifts that the diff shows as moves, long waits). Give the path to index.html. Mention the t=0 caveat, and that times are accurate to ±1 frame.