Jakubantalik/transitions.dev
GitHub当无持久化代理时,作为 Timeline Inspector Refine 的聊天内回退方案。通过轮询本地中继处理优化任务,支持扫描和应用,但需注意空闲消耗积分,应适时退出。
Install All Skills
npx skills add Jakubantalik/transitions.dev --all -g -y
More Options
List skills in collection
npx skills add Jakubantalik/transitions.dev --list
Skills in Collection (2)
refine/.agents/skills/refine-live/SKILL.md
npx skills add Jakubantalik/transitions.dev --skill refine-live -g -y
SKILL.md
Frontmatter
{
"name": "refine-live",
"description": "In-chat fallback for the Timeline Inspector Refine agent. Use when the user runs `\/refine live`, asks to \"refine live\", \"go live\", or answer refine jobs — but ONLY when no persistent agent is wired (no `npx transitions-refine live`). Prefer `npx transitions-refine live` for run-and-forget (relay spawns agent per click, no idle credit burn). This skill long-polls the relay, posts suggestions, handles scan\/apply jobs."
}
Refine Live
Two modes
Persistent (recommended — run and forget)
Run npx transitions-refine live from your project. The CLI starts the relay and wires REFINE_AGENT_CMD so the relay spawns your agent CLI per Refine click. No chat loop; idle = zero credit burn. Works hours later as long as the relay process keeps running. Stop with Ctrl-C (or npx transitions-refine stop).
In-chat loop (fallback — this skill)
Run /refine live in Cursor/Claude/Codex when the relay is up but has no REFINE_AGENT_CMD. You become the poller via GET /jobs/next. The Agent tab stays available only while you keep polling — each idle poll cycle consumes chat turns/credits. Say "stop refine" to exit.
Use the in-chat loop only when you cannot wire a persistent agent CLI.
Turn yourself into the LLM behind the Timeline Inspector's Refine button (in-chat fallback mode). While this loop runs, the panel's LLM tab is "available": each click sends one transition here, you reason about it, and your suggestions appear in the panel.
You are the poller. Nothing is installed — you just talk to a small local relay
(default http://localhost:7331) that the npx injector already started.
How it works
Browser (Refine, LLM tab) ──POST /jobs──► relay ──GET /jobs/next──► YOU
◄──GET /jobs/:id── relay ◄──POST /jobs/:id/result── YOU
The loop — stay live, but don't burn credits forever
Keep polling so the panel's LLM tab stays "available", but this loop costs chat turns/credits even while idle, so it is not truly run-and-forget — it has three exits, in priority order:
- Relay stop signal (authoritative).
GET /jobs/nextmay return200with{"stop": true}. The relay sends this when the user clicks Stop in the panel, or automatically after ~10 min with no jobs. Always honor it: stop looping immediately, tell the user the LLM tab will go unavailable and how to resume (/refine live), and end your turn. Never re-poll after a stop signal. - The user says so — "stop refine", "exit live", etc.
- Your own idle backoff (safety net). A long stretch of
204s is normal — it just means no one has clicked Refine yet — but to avoid spending credits on a forgotten loop, back off as idle grows instead of hammering immediately: re-poll right away for the first few empty cycles, then pause ~5s between polls, and after ~10 min of unbroken idle stop on your own (same as the relay's auto-stop) and tell the user how to resume. Any real job resets the backoff.
The relay reports the agent as "available" for ~120s after your last poll, so short pauses keep you live. A successful job always resets idle, so an active session never backs off.
-
Announce yourself once, before the first poll. The relay keeps a sticky Stop latch: after a panel Stop (or the idle auto-stop) it answers every
GET /jobs/nextwith{"stop": true}until a new agent explicitly resumes — so a stopped session can't silently come back. Clear the latch a single time at startup, then begin polling:curl -s -X POST http://localhost:7331/poller/startDo not call this again mid-loop (it would defeat a user's Stop). Only on a fresh
/refine live. -
Claim the next job (long-poll). This call blocks up to ~25s, then returns.
curl -s http://localhost:7331/jobs/next-
HTTP
204/ empty body → no work yet. Poll again, applying the idle backoff above (immediate at first, then ~5s pauses, then stop after ~10 min). -
HTTP
200with{"stop": true}→ the loop must end. Stop polling, tell the user the LLM tab is now unavailable and that/refine liveresumes it, and end your turn. Do not treat it as a job. -
HTTP
200with a job JSON → work to do. Shape:{ "id": "uuid", "request": { "label": "Resize + Color", "selector": ".box-resize", "mode": "llm", "refineType": "small", "timings": [ { "property": "width", "durationMs": 400, "delayMs": 0, "easing": "ease-out" }, { "property": "background", "durationMs": 400, "delayMs": 0, "easing": "ease-out" } ] } } -
If
request.kind === "scan"this is not a suggestion job — the panel is asking you to group the page's transitions by reading the source. Jump to## Scan jobsand returngroupsinstead of suggestions. -
If
request.kind === "apply"this is not a suggestion job — the user pressed Accept to write changes to their code. Jump to## Apply jobsand edit the source instead of posting suggestions. Everything below (refineType, steps 3–4) is for the normal Refine flow. -
refineTypechooses what kinds of suggestions to make (it mirrors the panel's two tabs). The tabs scan independently, so answer only the one you were asked for:"small"(or missing) → Small refinements: nudge the existing declarations toward the motion tokens only (step 3a). Do not propose a recipe swap here — that's the Replace tab's separate job."replace"→ Replace transition: suggest a whole-transition recipe swap only (step 3b). Do not propose motion-token tweaks — skip step 3a entirely.
-
-
(Optional) post progress so the panel shows what you're doing:
curl -s -X POST http://localhost:7331/jobs/<id>/status \ -H 'Content-Type: application/json' \ -d '{"message":"Matching to transitions.dev motion tokens…"}' -
Answer in ONE shot — speed matters. Each click should feel instant, so resolve the job from the data below plus what's already in this skill. Do not spawn subagents or run a broad codebase search, and do not open the transitions-dev
SKILL.md— its tokens and decision rules are inlined here.refineType === "small"→ step 3a only, with zero file reads.refineType === "replace"→ step 3b only; open at most the one recipe reference file you choose.
First, infer each declaration's usage from
label+selector(modal close, dropdown open, tooltip, badge, resize, color/theme change…). Match on intent, not the nearest number.3a. Motion-token tweaks (
refineType === "small"only — no file reads). Pick the token that fits the usage and propose a change only where the current value actually differs.- Durations: 40ms Stagger (per-item offset) · 80ms Micro (tooltip delay, shake segment) · 150ms Quick (modal/dropdown close, text swap, tooltip appear) · 250ms Fast (icon swap, dropdown/modal open, tabs slide, page slide) · 350ms Medium (panel/toast close) · 400ms Slow (panel open, skeleton reveal, input clear) · 500ms Very slow (emphasis, badge appear, text reveal, success check).
- Default easing — "Smooth ease out":
cubic-bezier(0.22, 1, 0.36, 1)(modal/dropdown/panel open+close, page slide, resize, position change). - Other on-grid easings — LEAVE UNCHANGED:
ease-out(tooltip),ease-in-out(icon/text swap, text reveal, skeleton reveal),linear(shimmer, pulse, spinner),cubic-bezier(0.34, 1.36, 0.64, 1)(badge pop),cubic-bezier(0.34, 3.85, 0.64, 1)(avatar return). - Nudge toward Smooth ease out: generic
ease,ease-in, or any hand-rolled cubic-bezier()/linear() that isn't a token above.
3b. Whole-transition recipe swap (
refineType === "replace"only — no file reads). Match the inferred usage to ONE recipe below (this list is the decision rules — no SKILL.md or reference-file read needed). Emit ONEkind: "replace"suggestion whosepatchcarries the motion-token duration/easing for the recipe's phase (open vs close) on the property that already transitions (or"all"), with areferencefield naming the file and the recipe intitle+reason. The patch only drives the live preview — exact keyframes/structure come from the user pasting that reference file, so you never need to open it. If no recipe genuinely fits the usage, return an emptysuggestionsarray with a shortsummary.- Card resize — a container changes width/height on a layout change (
01-card-resize.md) - Number pop-in — a number/digit updates (
02-number-pop-in.md) - Notification badge — a small dot/badge appears on a trigger (
03-notification-badge.md) - Text states swap — text content changes in place (
04-text-states-swap.md) - Menu dropdown — an anchored surface grows from its trigger (
05-menu-dropdown.md) - Modal open/close — a centered dialog scales up, softer scale-down on close (
06-modal.md) - Panel reveal — a surface slides into a region with a cross-blur (
07-panel-reveal.md) - Page side-by-side — slide between list↔detail or step 1↔step 2 (
08-page-side-by-side.md) - Icon swap — two icons cross-fade in the same slot (
09-icon-swap.md) - Success check — a checkmark celebration: fade + rotate + bob + stroke-draw (
10-success-check.md) - Avatar group hover — hover lifts an item in a horizontal stack (
11-avatar-group-hover.md) - Error state shake — invalid-input shake (
12-error-state-shake.md) - Input clear with dissolve — clearing a text field (
13-input-clear-dissolve.md) - Skeleton loader and reveal — placeholder pulses then swaps to real content (
14-skeleton-reveal.md) - Shimmer text — in-progress / "thinking" text shimmer (
15-shimmer-text.md) - Tabs sliding — a moving highlight across segmented options (
16-tabs-sliding.md) - Tooltip open/close — delayed fade+scale in, instant out (
17-tooltip.md) - Texts reveal — staggered blurred rise of stacked text lines (
18-texts-reveal.md) - Card hover tilt — 3D tilt toward the pointer (
19-card-tilt.md) - Plus to menu morph — a circular trigger becomes the surface it opens (
20-plus-menu-morph.md) - Accordion expand — a collapsible body grows/shrinks in height (
21-accordion.md)
Tie-break: prefer the lower-overhead recipe (card resize over panel reveal, dropdown over modal). Only propose a swap when the current declarations are clearly a hand-rolled version of a recipe or are missing the structure the usage calls for; if the transition already is the right recipe, return empty.
-
Post the result (this completes the job and renders cards in the panel):
curl -s -X POST http://localhost:7331/jobs/<id>/result \ -H 'Content-Type: application/json' \ -d '{ "summary": "Tightened the resize and softened the color fade.", "suggestions": [ { "id": "width-duration", "kind": "duration", "property": "width", "title": "Duration → Snappy (250ms)", "from": "400ms", "to": "250ms", "patch": { "property": "width", "durationMs": 250 }, "reason": "A size change reads as direct manipulation — snappy is more responsive than 400ms." } ] }'The example above is a
smalljob (token tweaks only). Areplacejob instead returns a singlekind: "replace"card as its only suggestion:{ "id": "replace-card-resize", "kind": "replace", "property": "width", "title": "Replace with Card resize", "from": "hand-rolled width tween", "to": "transitions.dev · Card resize", "patch": { "property": "width", "durationMs": 250, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" }, "reference": "transitions-dev/01-card-resize.md", "reason": "This is a width tween on layout change — the Card resize recipe handles it properly. Apply nudges the live timing; paste 01-card-resize.md (run `transitions apply card-resize`) for the full recipe." }If nothing should change, post
"suggestions": []with a shortsummary. If something goes wrong, report it instead:curl -s -X POST http://localhost:7331/jobs/<id>/error \ -H 'Content-Type: application/json' -d '{"message":"…"}' -
Go back to step 1. Keep looping, but honor the three exits from the loop section: a
{"stop": true}from the relay, the user telling you to stop, or your own idle backoff/auto-stop after ~10 min quiet. A real job resets idle. Whenever you do stop, tell them the LLM tab will go unavailable and how to restart (/refine live).
Scan jobs (group from source)
When a claimed job has request.kind === "scan", the panel wants you to turn a
flat list of DOM-detected transitions into components with phases. A naive
DOM scan only sees each element's current computed transition — it can't tell
open from close, and lists related elements (panel, backdrop, staggered items)
separately. You fix that by reading the source. The request looks like:
{
"id": "uuid",
"request": {
"kind": "scan",
"url": "http://localhost:5173/",
"raw": [
{ "label": "div.dropdown-panel", "selector": ".dropdown-panel",
"properties": ["opacity","transform"],
"timings": [{ "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }],
"cssRules": [
".dropdown .dropdown-panel { opacity: 0; transition: opacity 200ms ease-out 0ms, transform 200ms cubic-bezier(0.22, 1, 0.36, 1) 0ms; }",
".dropdown.is-open .dropdown-panel { opacity: 1; transform: translateY(0); }",
".dropdown.is-closing .dropdown-panel { transition: opacity 150ms ease-in 0ms; opacity: 0; }"
] }
]
}
}
Be fast. The raw.timings are already accurate for each element's current
on-screen state — treat them as ground truth and reuse them verbatim. Most raw
entries also carry cssRules: the CSS rules harvested live from the page
(CSSOM) that drive that element across all states (base + open + close), with
var() already resolved to concrete values.
Fast path — prefer cssRules over the filesystem. When an entry has
cssRules, they are authoritative and contain everything you need: the opposite
phase's timings live on a state-variant selector inside them (e.g.
.dd.is-closing .dd-panel, .modal[data-closing] .dialog), and the toggled
state is visible in those selectors. Derive grouping, phases, toggled state, and
opposite-phase timings directly from cssRules + timings — do not
glob/grep/read files for any element whose cssRules is non-empty; it only
wastes time. Only fall back to reading source for entries with an empty/missing
cssRules (CORS-locked sheets, styled-components, Tailwind, etc.), and even then
read the minimum.
Do this:
-
Identify each animated component the raw entries belong to (dropdown, modal, tooltip, accordion, drawer, toast…). The selectors/labels usually make this obvious — only read source (plain CSS / CSS Modules, styled-components/emotion, Tailwind, inline styles, Motion/Framer variants) when the grouping is genuinely unclear.
-
Split each component into phases — usually
openandclose(a hover-only component can be a single phase). The phase matching the current DOM reuses the provided timings; the opposite phase often lives on a different selector (.is-openvs.is-closing) with different timings — take it from the entry'scssRules(or, only if it has none, read source). Report both even though only one is in the DOM right now. -
List each phase's members — the elements that animate in that phase. Give each a stable
id, a humanlabel, a live-resolvable CSSselector, an optionaltoStatehint (the class/attribute that drives the phase, e.g..is-open), and itspropertyTimings. For the current-state phase, copy the providedraw.timingsverbatim; for the opposite phase, quote the real timings from the entry'scssRules(already var()-resolved) — or from source if it has none — never invent. -
Post the groups (this completes the job):
curl -s -X POST http://localhost:7331/jobs/<id>/result \ -H 'Content-Type: application/json' \ -d '{ "summary": "Grouped Dropdown into Open/Close.", "groups": [ { "id": "dropdown", "label": "Dropdown", "component": "src/Dropdown.tsx", "phases": [ { "id": "dropdown:open", "phase": "open", "label": "Open", "members": [ { "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-open", "propertyTimings": [ { "property": "opacity", "durationMs": 200, "delayMs": 0, "easing": "ease-out" }, { "property": "transform", "durationMs": 200, "delayMs": 0, "easing": "cubic-bezier(0.22, 1, 0.36, 1)" } ] } ] }, { "id": "dropdown:close", "phase": "close", "label": "Close", "members": [ { "id": "panel", "label": "Panel", "selector": ".dropdown-panel", "toState": ".is-closing", "propertyTimings": [ { "property": "opacity", "durationMs": 150, "delayMs": 0, "easing": "ease-in" } ] } ] } ] } ] }'If you can't confidently group anything, post
{"groups":[],"summary":"…"}— the panel keeps its flat DOM scan. Reserve/jobs/<id>/errorfor unexpected failures.
Then go back to step 1 of the loop.
Apply jobs (write to source)
When a claimed job has request.kind === "apply", the user accepted their current
timeline values and wants them written to the codebase. The request looks like:
{
"id": "uuid",
"request": {
"kind": "apply",
"label": "Dropdown · Close",
"selector": ".dropdown-panel",
"component": "src/Dropdown.tsx",
"group": "Dropdown",
"phase": "close",
"changes": [
{ "property": "opacity", "member": "Panel", "selector": ".dropdown-panel",
"from": { "durationMs": 300, "delayMs": 0, "easing": "ease" },
"to": { "durationMs": 150, "delayMs": 0, "easing": "cubic-bezier(0.4, 0, 1, 1)" } }
]
}
}
Do this:
-
Locate the real declaration in the source. The
selectoris a DOM-path hint, not necessarily the source selector. Use thecomponenthint and search by the label/class names; handle whatever the project uses: plain CSS / CSS Modules, styled-components or emotion template literals, Tailwind utilities (duration-300, arbitrary[transition-duration:300ms], or thetailwind.configtheme), inlinestyle={{ transition: … }}objects, and Motion/Framer variants. Match by thefromvalues to disambiguate.- If
phaseis set (e.g."open"/"close"), edit only that state's rule (the.is-openrule for open, the.is-closing/base rule for close) — not the other phase. Each change'smember+selectorsays which element.
- If
-
Edit each change's property to its
tovalues (durationMsms,easing,delayMsms) on the right member + phase. Keep the file's existing unit/format (0.25svs250ms) and touch only that property's timing. If a CSS variable / design token backs the value, update it at the single most sensible place. -
Minimal edit — no reformatting or unrelated changes.
-
Post the outcome (this completes the job):
curl -s -X POST http://localhost:7331/jobs/<id>/result \ -H 'Content-Type: application/json' \ -d '{"applied":true,"summary":"Set .t-modal transition to 150ms ease-in","files":["src/Modal.css:42"]}'If you cannot confidently find the declaration, post
{"applied":false,"summary":"<what you searched and why not found>"}(still aresult, not anerror). Reserve/jobs/<id>/errorfor unexpected failures.
Then go back to step 1 of the loop.
Suggestion shape (must match the panel)
Each suggestion object:
| field | meaning |
|---|---|
id |
unique within the job (e.g. "width-duration") — used to track "Applied" |
kind |
"duration" | "delay" | "easing" for token tweaks, or "replace" for a whole-transition swap (drives the card label) |
property |
the CSS property this targets, or "all" |
title |
short label shown on the card |
from / to |
human-readable before → after |
patch |
what actually gets applied — { "property", "durationMs"?, "delayMs"?, "easing"? }. Include only changed fields; property must match an input property (or "all"). For a replace, use the chosen recipe's recommended timing here so Apply still does something live. |
reference |
(replace only, optional) the transitions.dev reference file the user should paste for the full recipe, e.g. "transitions-dev/06-modal.md". |
reason |
one sentence of why, in usage terms |
The panel applies patch live in the browser via the property override. Values
are not written to source files — the user copies the ones they keep.
Notes
- Relay port:
http://localhost:7331unlessREFINE_RELAY_PORTwas changed. - Only LLM-mode jobs reach you; Deterministic-mode jobs are answered by the relay itself (nearest-token snapping) and never appear here. Whole-transition replace suggestions are therefore LLM-only — the deterministic path can't infer usage well enough to pick a recipe, so a Deterministic + "Replace transition" job just returns an empty result pointing the user back to the Agent tab.
- A
replacecard's Apply only changes the live timing in the patch. The recipe's structural parts (keyframes, extra properties, JS hooks) aren't applied in the browser — that's why the card points the user at the reference file to paste. - The relay errors a waiting job after ~120s, so answer promptly once you claim
one. The long-poll itself returning
204is normal — just poll again.
skills/transitions-dev/SKILL.md
npx skills add Jakubantalik/transitions.dev --skill transitions-dev -g -y
SKILL.md
Frontmatter
{
"name": "transitions-dev",
"description": "Production-ready CSS transitions for web apps. Use when implementing notification badges, dropdowns, modals, panel reveals, page transitions, card resizes, number pop-ins, text swaps, icon swaps, success checks, avatar group hovers, error state shakes, search\/input clear, skeleton loaders, shimmer text, sliding tabs, tooltips, staggered text reveals, card hover tilt, plus-to-menu morph, or accordions. Triggers on \"add a transition\", \"animate the dropdown\", \"make the modal open smoothly\", \"swap icon\", \"page slide\", \"stagger animation\", \"open \/ close transition\", \"make it animate\", \"fade between\", \"success animation\", \"form error\", \"shake on invalid\", \"hover lift\", \"avatar stack hover\", \"clear the search\", \"skeleton loader\", \"loading shimmer\", \"shimmer text\", \"sliding tabs\", \"segmented control\", \"tooltip\", \"reveal text\", \"tilt card\", \"3D hover tilt\", \"cursor glare\", \"plus to menu\", \"FAB morph\", \"accordion\", \"collapsible\", \"expand \/ collapse\", \"disclosure\". Also \"motion tokens\", \"scan for ad-hoc transitions\", \"replace hardcoded durations with motion tokens\", \"tokenize my animations\", and the commands transitions reveal, transitions review, transitions apply, transitions refine."
}
Transitions.dev
Twenty-one portable CSS transitions, each namespaced under t-* selectors with semantic CSS custom properties. Drop-in: paste the snippet, wire the documented HTML hooks, done. No framework dependencies, no demo-specific markup, and every snippet ships a prefers-reduced-motion guard.
Quick reference
| Transition | When to use | Reference |
|---|---|---|
| Card resize | Tween a container's width or height when its layout state changes. | 01-card-resize.md |
| Number pop-in | Re-enter each digit with a blurred slide when a number updates. | 02-number-pop-in.md |
| Notification badge | Slide a small badge onto a trigger and pop the dot. | 03-notification-badge.md |
| Text states swap | Swap text in place with a blurred up-and-down transition. | 04-text-states-swap.md |
| Menu dropdown | Open an origin-aware dropdown that grows from its trigger. | 05-menu-dropdown.md |
| Modal open / close | Scale-up modal dialog with a softer scale-down on close. | 06-modal.md |
| Panel reveal | Slide a panel into a region with a cross-blur. | 07-panel-reveal.md |
| Page side-by-side | Slide between two side-by-side pages (list ↔ detail, step 1 ↔ step 2). | 08-page-side-by-side.md |
| Icon swap | Cross-fade two icons in the same slot with blur and scale. | 09-icon-swap.md |
| Success check | Compose fade + rotate + Y-bob + path stroke-draw to celebrate a completed action. | 10-success-check.md |
| Avatar group hover | Distance-falloff lift on a row of items with a bouncy spring on return. | 11-avatar-group-hover.md |
| Error state shake | Per-segment cubic-bezier shake with auto-reverting border + message. | 12-error-state-shake.md |
| Input clear with dissolve | Fly-out + per-word streak when a text field is cleared. | 13-input-clear-dissolve.md |
| Skeleton loader and reveal | Pulse a placeholder, then cross-fade + cross-blur to the loaded content. | 14-skeleton-reveal.md |
| Shimmer text | Sweep a highlight band across muted text on a loop (pure CSS). | 15-shimmer-text.md |
| Tabs sliding | Slide the active pill between tabs in a segmented control. | 16-tabs-sliding.md |
| Tooltip open/close | Delayed fade+scale in, instant out (pure CSS). | 17-tooltip.md |
| Texts reveal | Staggered blurred rise for stacked text lines, quiet fade out. | 18-texts-reveal.md |
| Card hover tilt | Tilt a card in 3D toward the pointer with a cursor-tracked glare. | 19-card-tilt.md |
| Plus to menu morph | Morph a circular trigger into the menu / panel it opens. | 20-plus-menu-morph.md |
| Accordion expand | Grow / shrink a panel via grid-rows with a chevron flip. | 21-accordion.md |
Decision rules
When the user asks for a transition, match against the visible UI element first, then the verb:
- Trigger + small dot floating on top → notification badge.
- Trigger + surface that grows from it → dropdown (anchored, origin-aware) or modal (centered, no anchor).
- Surface that slides into a region of the page → panel reveal.
- Two screens, list ↔ detail or step 1 ↔ step 2 → page side-by-side.
- Element changes width or height → card resize.
- Element's text content changes in place → text states swap.
- Two icons in the same slot → icon swap.
- A number updates → number pop-in.
- Confirmation / success / "done" moment (checkmark, payment processed, file uploaded) → success check.
- Hovering an item in a horizontal stack (avatars, chips, segmented buttons, tag pills) → avatar group hover.
- Form validation error / "this is wrong" feedback (invalid field, wrong PIN, duplicate name) → error state shake.
- Clearing a text field (search box × button, filter reset) → input clear with dissolve.
- Placeholder that loads then swaps to real content (list row, card, profile header) → skeleton loader and reveal.
- In-progress / "thinking" text that should feel alive (loading label, streaming status) → shimmer text.
- Small horizontal set of mutually-exclusive options with a moving highlight (view switcher, segmented control, filter tabs) → tabs sliding.
- Hover/focus hint that appears over a trigger (icon tooltip, info bubble) → tooltip open / close.
- Stacked headline + supporting line entering with rhythm (hero copy, empty state, onboarding step) → texts reveal.
- Card / tile that should react in 3D to the pointer on hover (product card, cover art, membership card, with or without a light glare) → card hover tilt.
- Circular trigger that becomes the surface it opens (+ FAB grows into a menu / panel, compose button expands) → plus → menu morph. If the surface is a separate popover that merely grows from the trigger, use menu dropdown instead.
- Header with a collapsible body that grows / shrinks in height (settings group, FAQ, filter section, "show more", disclosure) → accordion expand.
- No clear match → fall back to
transitions revealand let the user pick. Don't guess.
If two transitions could fit, prefer the lower-overhead one (card resize over panel reveal, dropdown over modal, success check over a full modal celebration) unless the design clearly calls for the heavier surface. The success check is animation-only — if you also need to swap from a spinner to the check, pair it with icon swap.
Commands
The skill exposes four namespaced verbs the agent should recognise in addition to direct transition requests. Every command starts with transitions so the invocation never collides with verbs from other skills installed in the same project.
transitions reveal — list every transition
Trigger phrases: transitions reveal, "reveal the transitions", "list all transitions", "what transitions are in this skill", "show the transitions catalog".
Behaviour: print the twenty-one transitions as a numbered plain-text list — name, one-line summary, and the matching reference filename. Reuse the rows in ## Quick reference above; do not invent new copy. No project access.
transitions review — audit the project for fit
Trigger phrases: transitions review, "review my project", "audit my animations", "where would transitions.dev help", "find places to use this skill".
Behaviour:
- Search the workspace for indicators:
transition:declarations,@keyframes, hardcodedms/sdurations in style files, components matching the decision-rule patterns (modals, dropdowns, badges, search inputs, skeletons, tabs, tooltips, …). - For each hit, match against the decision rules and pick the single best-fit transition.
- Output a numbered list grouped by file:
path/to/Component.tsx:L42— looks like a dropdown opening, suggest menu-dropdown (05-menu-dropdown.md).- Skip ad-hoc transitions that already use a
t-*class.
- Do not edit anything. End with: "Run
transitions applyon any line to install the suggested transition."
transitions apply — install the best-fit transition
Trigger phrases: transitions apply, "apply a transition here", "add the right transition", "install transitions-dev here", "fix the animation on this element".
Behaviour:
- Read context: the currently-open file, the element nearest the cursor, surrounding CSS / JSX. If the user named a transition explicitly (e.g.
transitions apply menu-dropdown), use it. - Run the decision rules from
## Decision ruleson that context and pick one transition. If two could fit, prefer the lower-overhead one (same tie-breaker the existing rules use). - Surface a one-line proposal: "I'd apply menu-dropdown here because the element opens from a trigger and is anchored. Confirm to install?".
- On confirmation, follow the existing five-step procedure in
## Output formatverbatim (root block, snippet, hooks, reduced-motion guard, JS orchestration if needed). - If the agent can't pick a single transition with confidence, fall back to
transitions revealand ask the user to choose.
transitions refine — replace ad-hoc motion with the motion tokens
Trigger phrases: transitions refine, "refine my transitions", "scan for ad-hoc transitions", "replace hardcoded durations with motion tokens", "tokenize my animations", "tune the durations / easing", "audit my custom keyframes", "make the timing consistent", "align to the motion tokens".
Behaviour:
- Scan the whole project (not just dedicated stylesheets — also inline
style=/ CSS-in-JS, styled-components,<style>blocks, Tailwind arbitrary values likeduration-[300ms]) for ad-hoc motion:transition/animationshorthands and longhands, custom@keyframesblocks, hardcoded durations (…ms/…s), easing (cubic-bezier(...)or keywords), translate distances (px),scale(...), andblur(...). - For each value, infer what the motion does (modal close, dropdown open, tooltip, badge appear, text reveal, page slide, shake, …) from the surrounding selectors / component plus the
## Decision rules. For a@keyframesblock, read theanimationthat drives it and judge the keyframes' own duration/easing. - The key decision point is usage, not the raw number. Look the inferred usage up in
## Motion tokensand suggest the token whose documented usage matches — only when the usages line up. A 300ms modal close maps to--duration-quickbecause both are "modal close", even though the numbers differ. If a value's usage matches no token's usage, list it asno matching token usageand leave it untouched — never force a swap just because a number is close. - Output a numbered list grouped by file, showing only values that should change, each as
path/to/Component.css:L42—modal close: 300ms → var(--duration-quick) (150ms),ease → var(--ease-smooth-out). For keyframe-driven motion, suggest the token for the drivinganimation's duration/easing. - Do not edit anything. End with: "Confirm any line to apply the change, or run
transitions applyto install a full transition instead."
Motion tokens
The shared motion scale behind the twenty-one transitions — the same tokens the transitions.dev Motion tokens tab exposes. They ship at the top of _root.css, so once it's imported you can reference any of them as var(--…) (e.g. transition: transform var(--duration-fast) var(--ease-smooth-out)).
transitions refine maps each existing value to a usage below, then suggests the token to reference. Match on usage, not on the raw number — a 300ms modal close still maps to --duration-quick (150ms).
Durations
| Token | Value | Usage |
|---|---|---|
--duration-stagger |
40ms |
per-item stagger offset |
--duration-micro |
80ms |
tooltip/path delay, shake segment, large stagger |
--duration-quick |
150ms |
modal/dropdown close, text swap, tooltip appear |
--duration-fast |
250ms |
icon swap, dropdown/modal open, tabs sliding, page slide |
--duration-medium |
350ms |
panel close, toast close |
--duration-slow |
400ms |
panel open, skeleton content reveal, input clear |
--duration-very-slow |
500ms |
emphasis moments, badge appear, text reveal, success check |
Easings
| Token | Value | Usage |
|---|---|---|
--ease-smooth-out |
cubic-bezier(0.22, 1, 0.36, 1) |
modal/dropdown/panel open + close, page slide, resize, position change |
--ease-in-out |
ease-in-out |
icon swap, text swap, text reveal, skeleton reveal |
--ease-out |
ease-out |
tooltip open / close |
--ease-linear |
linear |
shimmer, skeleton pulse, spinner |
--ease-bounce |
cubic-bezier(0.34, 1.36, 0.64, 1) |
badge pop open |
--ease-bounce-strong |
cubic-bezier(0.34, 3.85, 0.64, 1) |
bouncy hover-out (avatar return) |
Distances
| Token | Value | Usage |
|---|---|---|
--distance-micro |
4px |
text swap |
--distance-small |
6px |
error shake (small segment) |
--distance-base |
8px |
badge diagonal reveal, page slide, error shake (large segment) |
--distance-medium |
12px |
text reveal |
--distance-large |
30px |
check badge appear |
Scales
| Token | Value | Usage |
|---|---|---|
--scale-large |
0.96 |
modal open / close |
--scale-medium |
0.97 |
dropdown open |
--scale-small |
0.98 |
tooltip open |
--scale-tiny |
0.99 |
dropdown close |
Blur
| Token | Value | Usage |
|---|---|---|
--blur-small |
2px |
panel reveal, icon swap, text swap, skeleton reveal, number pop-in |
--blur-medium |
3px |
page slide, text reveal |
--blur-large |
8px |
success check open |
Universal install
Copy _root.css into your project once and import it (or paste its :root block into your global stylesheet). It leads with the shared motion-token scale (--duration-*, --ease-*, --distance-*, --scale-*, --blur-* — see ## Motion tokens), followed by the semantic tunable variables for all twenty-one transitions. Every snippet reads from these names — --resize-*, --badge-*, --dropdown-*, --clear-*, --shimmer-*, --tabs-*, --tt-*, --stagger-*, --tilt-*, --morph-*, --acc-*, and the rest.
Each reference file also restates just the variables that snippet needs, so you can install a single transition without pulling the whole block. Don't duplicate the block — if _root.css is already imported, skip re-pasting any per-snippet :root.
The --pX-* source tokens used by the live demo at transitions.dev are intentionally not exported. Tunable values are renamed to semantic names so the user owns the design vocabulary. A few transitions (input clear, shimmer text, tabs, tooltip) carry color tokens that differ by theme — each reference file documents the html[data-theme="dark"] overrides.
Output format
When inserting a transition into the user's project:
- Install the variables from
_root.cssinto the user's global stylesheet, but only if they aren't already there — or just the per-snippet:rootblock from the reference file if installing a single transition. If the universal block is already imported, do not duplicate it. - Paste the chosen transition's CSS verbatim from the relevant reference file. Do not rewrite selectors, do not collapse the transition into shorthand, do not strip
will-change. The snippets are tuned and tested. - Wire the documented HTML hooks — class names (
.t-dropdown,.t-modal,.t-success-check,.t-avatar,.t-clear,.t-skel,.t-shimmer,.t-tabs,.t-tt,.t-stagger,.t-tilt,.t-morph,.t-acc, …) and state attributes (data-open,data-state,data-page,data-origin,aria-selected,aria-expanded,.is-open,.is-closing,.is-error,.is-shaking,.has-value,.is-clearing,.is-pulsing,.is-revealed,.is-shown,.is-hiding,.is-hover,.is-tilting). - Preserve the
@media (prefers-reduced-motion: reduce)block. Every snippet ships one. Removing it makes the component fail accessibility audits. - For transitions that need JS (dropdown, modal, text swap, number pop-in, page slide, success check, avatar group hover, error state shake, input clear, skeleton reveal, tabs sliding, texts reveal, card hover tilt, plus → menu morph, accordion expand), copy the small orchestration snippet from the reference file and adapt the selectors to the user's DOM. Keep the timing reads (
getComputedStyle(...)getPropertyValue("--…")) so durations stay in sync with the:rootvalues. Shimmer text and tooltip are pure CSS — no JS needed.
Keep the diff small: only edit the files needed to introduce the transition. Don't rename the user's existing variables, don't reformat unrelated CSS, don't pull in a motion library.
Common mistakes to avoid
- Stripping the close-state class cleanup on dropdown/modal — without the
setTimeoutthat removes.is-closing, the next open jumps from the closing scale instead of the resting pre-open scale. - Forgetting the reflow in the text swap, number pop-in, success check replay, and error state shake —
void el.offsetWidth(oroffsetHeight) between class/attribute removal and re-addition is what guarantees the animation replays. - Animating a single container instead of the inner pieces — for the badge, animate the dot, not the trigger; for page slide, animate the page sections, not the container.
- Replacing
transition: …withtransition: all— every snippet enumerates exact properties on purpose so unrelated style changes don't ride in for free. - Hardcoding the success check's
stroke-dasharray— the snippet ships20as a placeholder. Replace it withpath.getTotalLength()rounded up by 1 for your path, otherwise the stroke pre-reveals or over-draws. - Setting
transition-timing-functionin CSS for the avatar group hover — it has to be set inline in JS before the--shift/--scale-activewrites so the bouncy ease-out only applies onmouseleave. - Mixing
.is-errorand.is-shakinginto one class for the error state shake — keeping them orthogonal is what allows the shake to replay (remove → reflow → re-add) without flickering the whole error treatment. - Leaving the input clear glow on
mix-blend-mode: multiplyin dark mode — flip toscreen, bump--glow-opacityto ~0.85, and paint white gradients in JS. - Forgetting to write the tabs pill's first position without a transition — on first paint and resize, set
transform+widthwithtransition: none(then reflow + restore) or the pill animates in fromtranslateX(0)/width: 0. - Tracking the pointer on the tilting element itself for card hover tilt — bind
pointermoveto the flat outer.t-tiltwrapper, not.t-tilt-card, or the rotating edges slip under the cursor and the hover flickers. - Padding on the accordion grid track — put padding on
.t-acc-panel-inner, never on.t-acc-panel; padding on the0frtrack leaves a residual height strip so the panel never fully closes. - Morphing the accordion chevron's
dpath — CSSd:path interpolation is Chromium-only, so it never animates on mobile Safari / Firefox. Flip the chevron vertically (transform: scaleY(-1)) instead — it passes through a flat line at the midpoint just like the path morph and works everywhere. Keep the path symmetric about its viewBox centre and addvector-effect: non-scaling-strokeso the stroke stays constant through the flip. This is what the snippet ships.
Reference files
- 01-card-resize.md — Card resize
- 02-number-pop-in.md — Number pop-in
- 03-notification-badge.md — Notification badge
- 04-text-states-swap.md — Text states swap
- 05-menu-dropdown.md — Menu dropdown
- 06-modal.md — Modal open / close
- 07-panel-reveal.md — Panel reveal
- 08-page-side-by-side.md — Page side-by-side
- 09-icon-swap.md — Icon swap
- 10-success-check.md — Success check
- 11-avatar-group-hover.md — Avatar group hover
- 12-error-state-shake.md — Error state shake
- 13-input-clear-dissolve.md — Input clear with dissolve
- 14-skeleton-reveal.md — Skeleton loader and reveal
- 15-shimmer-text.md — Shimmer text
- 16-tabs-sliding.md — Tabs sliding
- 17-tooltip.md — Tooltip open/close
- 18-texts-reveal.md — Texts reveal
- 19-card-tilt.md — Card hover tilt
- 20-plus-menu-morph.md — Plus to menu morph
- 21-accordion.md — Accordion expand
- _root.css — the universal install block on its own, ready to import directly.


