/SKILL.md
Generates self-contained HTML/CSS animations of app features for walkthroughs, demos, and onboarding. Researches the target app via Chrome, interviews the user, generates a structured brief and animation HTML file, then enters an iterative freeze-inspect-feedback review loop until the user approves. Use when the user says "css animation", "animate this feature", "create a css walkthrough", "animation walkthrough", or wants to create a CSS-based visual demo of an app feature.
npx skillsauth add neonwatty/css-animation-skill css-animationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You create polished, self-contained HTML/CSS animations that mimic real app features. These are used for walkthroughs, onboarding demos, and marketing. They are NOT GIFs — they are resolution-independent, tiny file size, and easy to iterate on.
Determine which style the user needs during the Interview phase.
Phase 1: Research → Phase 2: Interview → Phase 3: Generate → Phase 4: Review Loop
Before generating anything, deeply understand the target app's visual system by navigating it in Chrome.
tabs_context_mcp to check existing tabstabs_create_mcpresize_window before navigating to the feature. This ensures you research the app's actual mobile layout, not the desktop version squeezed down. If targeting both, research desktop first at full size, then resize to mobile and re-extract the mobile layout.Use read_page, find, and javascript_tool to extract:
// Example: extract computed styles
const body = getComputedStyle(document.body);
JSON.stringify({
bg: body.backgroundColor,
color: body.color,
fontFamily: body.fontFamily
})
Record these as CSS custom properties (e.g., --bg: #0f0f0f).
For the specific feature to animate:
Use javascript_tool to extract exact positions:
const el = document.querySelector('.selector');
const rect = el.getBoundingClientRect();
`${rect.left}, ${rect.top}, ${rect.width}, ${rect.height}`
Take screenshots of:
These screenshots serve as visual reference for generation.
Ask the user focused questions to define what to animate. Use AskUserQuestion for each — one at a time, with multiple choice options.
"What style of animation should this be?"
- Feature Demo (Recommended): Before → Action → After. Shows one feature transformation.
- Carousel: Cycles through multiple app views with cross-fade transitions.
"What size should the animation target?"
- Desktop only (960×620px) — for landing pages, marketing, desktop walkthroughs
- Mobile only (360×640px) — for in-app tour tooltips, mobile onboarding
- Both — generates two files from the same brief, one per viewport
When Mobile or Both is selected:
<app>-<feature>.html (desktop), <app>-<feature>-mobile.html (mobile)"What specific feature or flow should the animation show?"
- [Options based on research phase findings]
- Other (user describes)
"What's the key moment — the visual 'wow' of this animation?"
- [Options relevant to the chosen feature]
- Other
"Is there anything specific to emphasize or avoid showing?"
- Show everything as-is
- Emphasize specific elements (user specifies)
- Hide certain elements (user specifies)
"Where should I save the animation files?"
- Current directory: [show cwd path]
- Desktop
- Other (user specifies path)
Stop after 3-6 questions — when you have enough context to generate. Don't over-interview.
This phase produces two artifacts: a brief (markdown) and an HTML animation file.
Write a structured markdown document capturing everything needed to generate or regenerate the animation. Save as <app>-<feature>-brief.md in the user's chosen directory.
Brief format:
---
app: <App Name>
feature: <Feature Name>
style: feature-demo | carousel
target: desktop | mobile | both
output_file: <app>-<feature>.html
output_file_mobile: <app>-<feature>-mobile.html # only when target is mobile or both
---
# Design Language
- Background: <hex>
- Surface: <hex>
- Border: <hex>
- Accent: <hex> (<name>)
- Text: <hex>
- Text dim: <hex>
- Heading font: <font> (serif/sans-serif)
- Body font: <font> (sans-serif)
- Border radius: <N>px
- Additional colors: <name>: <hex> (for groups, statuses, categories, etc.)
# Layout (Desktop)
- Stage: 960x620px
- [Container hierarchy description]
- [Element positions with exact pixel coordinates]
- [Circular elements: center point (x,y), radius R]
- [Rectangular containers: origin (x,y), width, height]
- [Item sizing: WxH px]
# Layout (Mobile) <!-- only when target is mobile or both -->
- Stage: 360x640px
- [Simplified container hierarchy — no sidebars, single-column]
- [Reduced element count: 8–12 elements max]
- [Element positions recalculated for smaller stage]
- [Larger relative element sizing: e.g., 36x36px avatars instead of 30x30px]
- [Elements or sections omitted from desktop version and why]
# Animation Plan
## Style: Before → Action → After
(or: ## Style: Carousel with N scenes)
### Before State
- [Element positions, visual state, text content]
- [For circular layouts: N items, angular spacing, starting angle]
### Action (feature-demo only)
- [User action: cursor movement, button click]
- [Intermediate states: loading, processing]
### After State
- [New positions, trigonometrically calculated for circles]
- [Visual changes: colors, borders, labels, badges]
- [Text content changes]
### Scene N (carousel only)
- [What's visible in this scene]
- [Entry animations for elements]
- [Duration and transition to next scene]
### Timing
- Total loop: <N>s
- [Phase-by-phase durations]
- Easing: cubic-bezier(0.34, 1.56, 0.64, 1) for spring motion
- Easing: ease-in-out for fades
Show the brief to the user before generating HTML:
"Does this brief capture the animation correctly?"
- Yes, generate the animation
- Needs adjustments (user specifies)
Iterate on the brief until the user approves it, then proceed to 3b.
Using the brief as your spec, generate a self-contained HTML file. Save as <app>-<feature>.html in the user's chosen directory.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><App> – <Feature> Animation</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=...');
/* ALL CSS here — no external stylesheets */
</style>
</head>
<body>
<div class="stage" id="stage">
<!-- All visual elements -->
</div>
<script>
// Minimal JS: setTimeout loop controller + class toggling ONLY
</script>
</body>
</html>
1. Trigonometric positioning for circular layouts.
NEVER eyeball positions on circles. ALWAYS calculate:
left = centerX + R * cos(angle_radians) - (elementWidth / 2)
top = centerY + R * sin(angle_radians) - (elementHeight / 2)
Where:
R = visible radius of container + (elementSize / 2), so the element's inner edge touches the container perimeterangle_i = startAngle + i * (2 * PI / N), starting from -PI/2 (top center)radians = degrees * PI / 180Example for 7 items on a circle with center (140, 160) and R=95, items are 30x30px:
Item 0: angle = -90° → left = 140 + 95*cos(-90°) - 15 = 125, top = 160 + 95*sin(-90°) - 15 = 50
Item 1: angle = -38.6° → left = 140 + 95*cos(-38.6°) - 15 = 199, top = 160 + 95*sin(-38.6°) - 15 = 86
...etc for all N items
2. Rectangular edge positioning.
For items along rectangle edges, distribute evenly:
containerLeft + containerWidth * (i + 1) / (numItemsOnEdge + 1)3. Self-contained HTML. No external dependencies except Google Fonts @import. All CSS in <style>, all JS in <script>. Zero build step.
4. CSS transitions for element movement.
.element {
position: absolute;
transition: left 1.2s cubic-bezier(0.34, 1.56, 0.64, 1),
top 1.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
5. JS only for loop control and class toggling.
CRITICAL: Never use inline styles (element.style.opacity = '1') in JS reset/update functions. Inline styles override CSS class rules and cause state bugs (e.g., a "hidden" element that stays visible because an inline style trumps the CSS class). Use class toggling exclusively.
const stage = document.getElementById('stage');
function runCycle() {
// Reset — class toggling only, no inline styles
stage.classList.remove('clicking', 'processing', 'optimized', 'complete');
// Reset text content to before-state values
setTimeout(() => stage.classList.add('clicking'), 500);
setTimeout(() => {
stage.classList.remove('clicking');
stage.classList.add('processing');
}, 900);
setTimeout(() => {
stage.classList.remove('processing');
stage.classList.add('optimized');
// Update text content to after-state values
}, 1500);
setTimeout(() => stage.classList.add('complete'), 3000);
setTimeout(runCycle, 6000); // Loop
}
setTimeout(runCycle, 300);
Default to fast timing. Users almost always want the animation faster than your first instinct. Start with snappy defaults:
6. CSS @keyframes for non-interactive animations — cursor movement, badge entrance, spinner rotation. Use the animation property on the relevant elements.
7. Stagger transitions with transition-delay on individual elements (0.04–0.08s apart) for sequential movement that feels natural.
8. Stage dimensions by target.
border-radius: 12pxborder-radius: 16px (portrait orientation)9. Hidden elements must use visibility: hidden alongside opacity: 0. On dark backgrounds, JPEG screenshot compression creates visible artifacts for opacity-0 elements — they appear as faint ghosts. Always pair:
.element-hidden {
opacity: 0;
visibility: hidden;
transition: opacity 0.5s ease-out, visibility 0s 0.5s; /* visibility delays on hide */
}
.element-hidden.visible {
opacity: 1;
visibility: visible;
transition: opacity 0.5s ease-out, visibility 0s 0s; /* visibility instant on show */
}
For elements that should be completely removed from flow when hidden (like placeholder text), use display: none via a parent class toggle:
.stage.active .placeholder { display: none; }
10. Verify content fits the stage. After generating, inject JS to check that all content fits within the stage bounds before entering the review loop:
var content = document.querySelector('.content');
var stage = document.getElementById('stage');
JSON.stringify({ stageH: stage.offsetHeight, contentH: content.scrollHeight, fits: content.scrollHeight <= stage.offsetHeight - content.offsetTop })
If content overflows, compact spacing (reduce padding, margins, font sizes) or increase stage height before proceeding to review. Don't waste review cycles on overflow bugs.
opacity keyframes to show/hide (fade in → hold → fade out)animation-delay for staggered entry@keyframes with percentage-based timing for scene visibilitydiv with position: absolute; inset: 0;.stage.optimized .element { left: ...; top: ...; }stage.classList.add('optimized')) triggers CSS transitionsWhen generating mobile animations (360×640px stage):
When generating both variants from a single brief:
<app>-<feature>.html)<app>-<feature>-mobile.html) as a separate fileThis is the core quality mechanism. After generating the animation HTML, enter an iterative review loop until the user approves.
lsof -i :8765
python3 -m http.server 8765 --directory <output-directory> &
resize_window before navigating. This ensures you see the animation at the intended scale, not stretched across a desktop window.http://localhost:8765/<filename>.html?v=<timestamp>ALWAYS append ?v=<Date.now()> to bust the cache after every edit.
When reviewing both variants: Review the desktop animation first at full browser size, then resize to mobile and review the mobile variant. Keep reviews separate — don't mix feedback between variants.
Inject JavaScript to stop the animation and set a specific state:
// Stop ALL pending timers
var id = window.setTimeout(function(){}, 0);
while (id--) { window.clearTimeout(id); }
// Reset stage
var stage = document.getElementById('stage');
stage.classList.remove('clicking', 'processing', 'optimized', 'complete');
// Hide cursor
var cursor = document.querySelector('.cursor');
if (cursor) { cursor.style.animation = 'none'; cursor.style.opacity = '0'; }
// Hide badges/overlays
var badge = document.querySelector('.optimized-badge');
if (badge) { badge.style.display = 'none'; }
Then set the target state:
stage.classList.add('optimized'); and update text contentstage.classList.add('optimized', 'complete'); and show badgeTake a screenshot after setting each state. Present to the user.
Feature Demo:
Carousel:
For EACH inspected state, use AskUserQuestion:
"How does the [BEFORE / AFTER / Scene N] state look?"
Options:
- Looks good
- Element positions are off (not on perimeters, wrong placement)
- Colors or styling need adjustment
- Layout or spacing needs changes
- Text content is wrong
- [Other]
Enable multiSelect: true if multiple issues are likely.
When the user reports an issue:
?v=<new-timestamp>Repeat Steps 3-4 until the user says "Looks good" for every state.
Once ALL frozen states are individually approved:
"How does the full animation look in motion?"
Options:
- Approved — looks great!
- Timing needs adjustment
- Transitions need work (easing feels wrong, stagger off)
- Something else needs fixing
Timing is always iterative. Expect 2-4 rounds of timing adjustment — this is normal, not a sign of a bad first attempt. The goal is to converge quickly by asking specific questions.
When the user says timing needs adjustment, ask specifically which phases:
"What about the timing needs adjustment?"
Options (multiSelect: true):
- Initial state too long (before the action starts)
- Initial state too short (need more time to see the 'before')
- Action/stagger too slow (elements appear too slowly)
- Action/stagger too fast (can't follow what's happening)
- Final hold too long (loop feels sluggish)
- Final hold too short (not enough time to absorb the result)
Apply the changes, reload with cache-buster, and re-ask:
"How does the updated timing feel?"
Options:
- Approved — looks great!
- Still needs adjustment
If "Still needs adjustment" → ask the specific phase question again. Repeat until approved.
Timing tuning tips:
Inspect static states first, then evaluate motion.
Positioning and layout bugs are much easier to see in frozen frames. Timing and easing bugs only appear during playback. Fix the visual bugs first, then tune the motion.
lsof -i :<port>.?v=<timestamp>. If still stale, try a new Chrome tab.read_page or javascript_tool to inspect the live DOM and computed styles.cubic-bezier(0.34, 1.56, 0.64, 1) makes elements feel physical and alive.transition-delay (0.04–0.08s per element) looks far better than simultaneous movement.development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.