CSS and Layout skill 9 of 12
CSS Animation SKILL.md: Fix Transitions, Reduced Motion and Jank
The CSS Animations and Transitions skill makes Claude, Cursor or another AI agent find and fix transition: all, layout properties animated, missing or blunt reduced-motion rules, unitless durations, display fades without allow-discrete and endless loops. A scanner reports each case by file and line, the agent edits only flagged lines, and you approve each batch.
css-animations-transitions/SKILL.md, version 1.0.0, 4,135 words, plus scripts/motion_check.py and two reference files.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 7 November 2026. Updated 7 November 2026. Free to use under CC BY 4.0.
An agent works on motion in this order: confirm scope and the motion rules the project already has, run the scanner, fix declarations that are invalid or do nothing, move animated properties onto the cheap ones, write the reduced-motion rule for every animation that remains, handle entry and exit of hidden elements, and re-run the scanner plus one emulated check. The scanner reads text. It cannot see animations started by JavaScript or timing it cannot parse, so it reports patterns for a person or agent to confirm.
Reusable code is in references/patterns.md. Support notes, tool notes and the rival comparison are in references/appendix.md.
When to use this skill, and when not to
Use it when an animation stutters, a transition does nothing, an element cannot fade out because it is display: none, a design has no reduced-motion handling, or a stylesheet is full of transition: all.
| Request | Use instead |
|---|---|
Animating a custom property, @property, var() inside keyframes | css-custom-properties.md |
Opening and closing <dialog> and popovers, focus, light dismiss | dialog-and-popover.md |
| Focus rings and what they must show | keyboard-focus.md |
| Announcing changes to screen readers | aria-live-announcements.md |
| GSAP, Framer Motion, the Web Animations API, Lottie | Out of scope. Name the tool and stop |
| Scroll-driven animation, view transitions, SVG animation | Out of scope. No source read for this skill covers them |
| Choosing the brand's motion personality | Out of scope. Durations and easing come from the user |
Guardrails
- Propose, then change. Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
- Durations and easing come from the user. The agent never picks a value for the project. It offers the options in step 5, the user chooses, and the choice goes into a token or a shared rule.
- Reduced motion changes what people see. Show the reduced version of each animation to the user before applying it. Never remove an animation that carries information (a progress indicator, a validation shake that is the only error cue) without agreeing a replacement.
- Not a conformance claim. WCAG success criteria are read here as guidance. A statement that a product conforms needs a human accessibility review and is out of scope. This is not legal advice.
- Data handled. The scanner prints file, line, code and a fixed message, never source text. Class names and comments can reveal unreleased features: report selectors only when the user needs them, and never paste stylesheets into prompts.
- Nothing is invented. Every rule carries a source and a date. Anything that depends on a browser version carries verify before relying on this. The sources for compositing advice are old (step 4): measure instead of trusting them.
- Refuses to: add a global
animation: none !importantreset as the whole fix, addwill-changeto silence a jank report, claim a speed-up without a before and after measurement, or declare a fix verified without re-running the scanner.
Inputs the agent must have before starting
Ask for every missing item. Do not assume.
| Input | Ask exactly |
|---|---|
| Scope | "Which files or folders should I review? Give paths." |
| Symptom | "What goes wrong: stutter, a transition that does nothing, an element that cannot fade out, no reduced-motion support, or a general review?" |
| Motion rules | "Does the project have motion tokens or a motion guideline? Give the file or say none." |
| Browsers | "Which browsers and minimum versions must work? Say if Firefox or Safari must support @starting-style and allow-discrete." |
| JavaScript | "Does any script listen for animationend or transitionend, toggle classes to start motion, or use the Web Animations API? Give paths or say none." |
| Essential motion | "Which animations carry information a user needs (progress, loading, error cues)?" |
| Approval | "May I edit on a branch in batches of one file and show each diff first?" |
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as css-animations-transitions/SKILL.md.
---
name: css-animations-transitions
description: "Audits and fixes CSS transitions and animations: transition: all, layout properties animated, missing or blunt prefers-reduced-motion, unitless durations, display transitions without allow-discrete, broad will-change, endless loops. Runs a scanner, then edits flagged lines only. Use for 'janky animation', 'prefers-reduced-motion', 'transition not working', '@starting-style', fade out display none. Do NOT use for JavaScript animation libraries, WAAPI or GSAP, custom property typing (use css-custom-properties), scroll-driven animation or SVG."
license: CC-BY-4.0
metadata:
author: Afeez Adebayo
author_url: https://afeezadebayo.com
author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
author_x: https://x.com/za_icon
source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/
category: frontend-development
subcategory: css-layout
version: "1.0.0"
created: "2026-11-07"
updated: "2026-11-07"
---
# CSS Animations and Transitions: Motion That Is Cheap, Short and Optional
An agent works on motion in this order: confirm scope and the motion rules the project already has, run the scanner, fix declarations that are invalid or do nothing, move animated properties onto the cheap ones, write the reduced-motion rule for every animation that remains, handle entry and exit of hidden elements, and re-run the scanner plus one emulated check. The scanner reads text. It cannot see animations started by JavaScript or timing it cannot parse, so it reports patterns for a person or agent to confirm.
Reusable code is in `references/patterns.md`. Support notes, tool notes and the rival comparison are in `references/appendix.md`.
## When to use this skill, and when not to
Use it when an animation stutters, a transition does nothing, an element cannot fade out because it is `display: none`, a design has no reduced-motion handling, or a stylesheet is full of `transition: all`.
| Request | Use instead |
|---|---|
| Animating a custom property, `@property`, `var()` inside keyframes | `css-custom-properties.md` |
| Opening and closing `<dialog>` and popovers, focus, light dismiss | `dialog-and-popover.md` |
| Focus rings and what they must show | `keyboard-focus.md` |
| Announcing changes to screen readers | `aria-live-announcements.md` |
| GSAP, Framer Motion, the Web Animations API, Lottie | Out of scope. Name the tool and stop |
| Scroll-driven animation, view transitions, SVG animation | Out of scope. No source read for this skill covers them |
| Choosing the brand's motion personality | Out of scope. Durations and easing come from the user |
## Guardrails
1. **Propose, then change.** Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Durations and easing come from the user.** The agent never picks a value for the project. It offers the options in step 5, the user chooses, and the choice goes into a token or a shared rule.
3. **Reduced motion changes what people see.** Show the reduced version of each animation to the user before applying it. Never remove an animation that carries information (a progress indicator, a validation shake that is the only error cue) without agreeing a replacement.
4. **Not a conformance claim.** WCAG success criteria are read here as guidance. A statement that a product conforms needs a human accessibility review and is out of scope. This is not legal advice.
5. **Data handled.** The scanner prints file, line, code and a fixed message, never source text. Class names and comments can reveal unreleased features: report selectors only when the user needs them, and never paste stylesheets into prompts.
6. **Nothing is invented.** Every rule carries a source and a date. Anything that depends on a browser version carries **verify before relying on this**. The sources for compositing advice are old (step 4): measure instead of trusting them.
7. **Refuses to:** add a global `animation: none !important` reset as the whole fix, add `will-change` to silence a jank report, claim a speed-up without a before and after measurement, or declare a fix verified without re-running the scanner.
## Inputs the agent must have before starting
Ask for every missing item. Do not assume.
| Input | Ask exactly |
|---|---|
| Scope | "Which files or folders should I review? Give paths." |
| Symptom | "What goes wrong: stutter, a transition that does nothing, an element that cannot fade out, no reduced-motion support, or a general review?" |
| Motion rules | "Does the project have motion tokens or a motion guideline? Give the file or say none." |
| Browsers | "Which browsers and minimum versions must work? Say if Firefox or Safari must support `@starting-style` and `allow-discrete`." |
| JavaScript | "Does any script listen for `animationend` or `transitionend`, toggle classes to start motion, or use the Web Animations API? Give paths or say none." |
| Essential motion | "Which animations carry information a user needs (progress, loading, error cues)?" |
| Approval | "May I edit on a branch in batches of one file and show each diff first?" |
## Workflow
### Step 1: Run the scanner
```
python3 scripts/motion_check.py src | head -40
```
Read only its output. Do not read the script. The first line holds the counts. `--strict` exits 1 on warnings. It skips `node_modules`, `dist`, `build`, `.git`, `.next` and `vendor`, and reads `.css`, `.scss`, `.sass`, `.less`, `.html`, `.htm`, `.vue`, `.svelte` and `.astro`.
| Code | Level | Meaning | Fix |
|---|---|---|---|
| `no-reduced-motion` | error | Animation or transition code and no `prefers-reduced-motion` query anywhere scanned | Step 3 |
| `duration-no-unit` | error | A non-zero duration with no `s` or `ms`: invalid | Step 5 |
| `discrete-no-allow-discrete` | error | `display` or `content-visibility` transitioned without `allow-discrete` | Step 6 |
| `transition-all` | warn | `transition: all` or `transition-property: all` | Step 5 |
| `layout-property-animated` | warn | A layout property in a transition or in `@keyframes` | Step 4 |
| `behavior-before-shorthand` | warn | `transition-behavior` written before the `transition` shorthand | Step 6 |
| `will-change-broad` | warn | `will-change` on `*`, `html`, `body`, `div`, `span` or `:root` | Step 4 |
| `reduced-motion-blunt-reset` | warn | `!important` duration or `animation` reset on a broad selector inside a reduced-motion query | Step 3 |
| `infinite-no-pause` | warn | `infinite` animation whose selector has no reduced-motion rule and no `animation-play-state` | Step 3 |
| `animation-name-undefined` | warn | `animation-name` with no `@keyframes` in the scanned files | Step 5 |
| `will-change-static` | info | `will-change` in a normal rule | Step 4 |
| `keyframes-unused` | info | `@keyframes` never referenced | Step 5 |
The scanner splits on text. A hit in a string or template is a false positive: read the line. It cannot see an `@keyframes` defined in a file outside the scan, which produces `animation-name-undefined`. It checks reduced motion at file-set level, so one query in one file silences `no-reduced-motion` for all files: step 3 checks each animation by hand.
### Step 2: Fix invalid and inert declarations
| Finding | Why it does nothing | Fix |
|---|---|---|
| `duration-no-unit` | `opacity .3` has no unit, so the declaration is invalid | `0.3s` or `300ms`. Only `0` may omit the unit |
| `animation-name-undefined` | The name has no `@keyframes` in scope, so nothing runs | Define it, correct the spelling, or ask where it lives |
| `keyframes-unused` | Dead code | Report. Delete only on approval, since JavaScript can set the name |
| A transition that never starts | The property has no start value in the same rule set, or the element is inserted already in its final state | Step 6 for new elements. Otherwise declare the start value |
### Step 3: Reduced motion
Sources: WCAG 2.2, Understanding SC 2.3.3, Animation from Interactions, fetched 7 November 2026; web.dev, prefers-reduced-motion, last updated 11 March 2019, fetched 7 November 2026.
| Fact | Consequence |
|---|---|
| SC 2.3.3 is Level AAA: motion animation triggered by interaction can be disabled, unless it is essential (WCAG) | Aim for it, but do not describe a site as failing AA because it lacks it |
| Sufficient techniques are C39 (the `prefers-reduced-motion` media query) and SCR40 (the same query in JavaScript) (WCAG) | Both are covered in `references/patterns.md`, sections 1 and 2 |
| Colour, opacity and blur changes that do not alter perceived size, shape or position are outside the glossary definition, though an erratum removes the blur exclusion (WCAG) | A fade is usually safe to keep. A blur needs a check |
| SC 2.2.2 covers animation the page starts by itself, and one animation can fail both criteria (WCAG) | Read SC 2.2.2 for its time threshold before quoting one: it was not fetched for this skill |
| `reduce` means minimise non-essential movement, ideally to none (web.dev) | Remove movement, keep feedback. Do not remove everything |
| The `*`, `::before`, `::after` reset with `!important` is a workaround for user stylesheets, and it uses 1 ms because some sites rely on `animationend` (web.dev) | Do not ship it as the site's own fix |
| The media query cannot stop motion started by the Web Animations API (web.dev) | Check JavaScript separately (step 7) |
Classify every animation the scanner and a read of the code find:
| Class | Examples | Under `reduce` |
|---|---|---|
| Essential | Progress bar, indeterminate loading, the only error cue | Keep, with the movement removed or shrunk: agree the replacement with the user |
| Movement | Translate, scale, rotate, parallax, bounce, slide-in | Remove: set the end state at once |
| Non-movement | Opacity or colour fade | Keep, and shorten if the user wants |
| Autoplay loop | Infinite spin, marquee, pulsing badge | Stop, or pause with a control |
Rules:
1. Write the reduced rule next to the animation it changes, inside a `@media (prefers-reduced-motion: reduce)` block, selector by selector. This is the pattern at `references/patterns.md` section 1. For new code, the opt-in form (motion inside `no-preference`) is the safer default: it is a house rule, and web.dev shows both forms.
2. Setting `animation: none` means `animationend` never fires. Run `grep -rnE "animationend|transitionend|onAnimationEnd|onTransitionEnd" src`. Every handler found needs a fallback that runs without the event, or the reduced rule must keep a near-zero duration for that element instead.
3. `reduced-motion-blunt-reset` is fixed by replacing the global reset with per-selector rules, not by deleting it and leaving nothing.
4. `infinite-no-pause`: stop the loop under `reduce`. If the loop is essential, ask the user for the replacement (guardrail 3).
### Step 4: Move animated properties onto cheap ones
Sources: web.dev, Stick to compositor-only properties and manage layer count, last updated 20 March 2015, fetched 7 November 2026; Josh Comeau, An Interactive Guide to CSS Transitions, fetched 7 November 2026. **Verify before relying on this**: the web.dev article is more than ten years old, so confirm in the browser's performance panel, in the browsers the user named.
| Fact | Consequence |
|---|---|
| web.dev names only `transform` and `opacity` as compositor-only | Prefer them. Replace a width, height, top or left animation with `transform` where the visual result is the same |
| For other properties the article suggests FLIP, which remaps the change onto `transform` and `opacity` | Offer FLIP only when a layout change must animate. Sketch in `references/patterns.md`, section 5 |
| At 60 fps a frame is about 16.6 ms (Comeau) | A layout property animated on every frame can break that budget on a slow device |
| `transform` moves between pixels, while `margin-top` rounds to whole pixels (Comeau) | A `margin` animation can look steppy even when it is fast |
| The article suggests about 4 to 5 ms of compositing time as a target during scrolling or transitions | Treat it as a prompt to measure, not as a pass mark |
`will-change` rules:
| Situation | Do |
|---|---|
| On `*`, `html`, `body` or a tag selector | Remove. web.dev shows a `*` rule with `will-change` as its bad example |
| On a class, in a static rule | Report as info. Keep only if the user can show the first frame stutters without it |
| Needed for one animation | Add it just before the motion starts and remove it after, as in `references/patterns.md`, section 6 |
| "How many layers is too many?" | No source read gives a number. Open the Layers panel in browser DevTools and ask the user what the target device is |
The article names Chrome DevTools: in the Paint profiler, select a frame and open the Layers tab to see each layer and why it exists. Menu names change, so **verify before relying on this** in the current DevTools.
### Step 5: Transitions: properties, durations and easing
| Rule | Detail |
|---|---|
| List the properties | Replace `all` with the exact list (Comeau: "It pays to be really precise with the properties we animate") |
| Units | `s` or `ms` on every non-zero duration |
| Define the keyframes you reference | One name, one `@keyframes` |
| Hover triggers | Put the trigger on the parent and animate a child, so the moved element does not slide out from under the pointer and retrigger hover (Comeau) |
| Dropdown gaps | A `transition-delay` on the close lets the pointer cross a gap (Comeau gives 300 ms in a demo) |
Durations and easing come from the user (guardrail 2). If the project has no motion tokens, offer these as options and say where each comes from. Neither source is a standard.
| Option | Source |
|---|---|
| 125 ms for a snappy hover-in, 450 ms for a relaxed hover-out, 250 to 750 ms in general demos | Josh Comeau, demos, fetched 7 November 2026 |
| About 150 to 300 ms for UI elements, 300 to 500 ms for page transitions | moderndegree, motion-animation skill (a rival's claim), fetched 7 November 2026 |
| `ease-out` for elements entering, `ease-in` only when leaving fully offscreen or becoming invisible, `ease-in-out` for loops | Josh Comeau, fetched 7 November 2026 |
Put the chosen values in tokens (`css-custom-properties.md`, step 5) and read them with `var()` in `transition` values.
### Step 6: Entry, exit and `display`
Source: Chrome for Developers, Four new CSS features for smooth entry and exit animations, last updated 16 August 2023 with a note dated 13 September 2024, fetched 7 November 2026. **Verify before relying on this** for every browser outside Chrome: the page gives Chrome versions only and no Baseline status.
| Fact | Consequence |
|---|---|
| Discrete properties such as `display` transition only with `transition-behavior: allow-discrete`, set for each transitioning property | Add it to every rule that transitions `display` or `content-visibility` |
| The `transition` shorthand resets `transition-behavior` to `normal` | Write `transition-behavior` after the shorthand: `behavior-before-shorthand` |
| In a transition, `display` switches at the start when animating in and at the end when animating out | A fade-out keeps the element visible until the fade ends |
| `@starting-style` gives the before-open state the browser reads when an element first renders (Chrome 117) | Use it for fade-in on insert and on `[open]` or `:popover-open` |
| A note says `@starting-style` rules must come after the normal declarations from Chrome 130 | Put the block after the base rule |
| Without `overlay` in the transition list, a popover or dialog snaps out of the top layer and the fade-out is not seen | Add `overlay` with `allow-discrete` for top-layer elements |
| With `animation-fill-mode: forwards` the element stays `display: none`. Removing the node needs a timeout so the animation can finish | Ask before removing nodes from script |
Pattern: `references/patterns.md`, section 4. For focus return and light dismiss on dialogs and popovers, hand over to `dialog-and-popover.md`.
### Step 7: JavaScript and the rest
| Check | Do |
|---|---|
| Script adds classes to start motion | Make sure the class change cannot start a movement animation under `reduce`: use the matchMedia pattern (`references/patterns.md`, section 2) |
| Script uses the Web Animations API | The CSS query does not stop it (web.dev). Gate it with `matchMedia` |
| Script waits for `animationend` | Step 3, rule 2 |
| Script removes nodes after an exit | Wait for the event or a timeout equal to the duration, from the same token |
### Step 8: Verify and report
1. Re-run the scanner. Errors must be 0. Every warning is fixed or listed with a reason.
2. Emulate reduced motion in the browser or test runner (`references/appendix.md`, section 3) and load one page per fixed finding. Report any movement still visible.
3. Compare before and after in the browser's performance panel for each animation moved onto `transform` or `opacity`. Report the numbers, or say it was not measured.
4. State what is untested: JavaScript-started motion, browsers outside the matrix, low-end devices, screen reader behaviour.
5. Use the report template. Do not echo unchanged CSS back.
## Worked example
An invented `motion.css` of 12 lines: `will-change` on `*`, `transition: all`, a height transition and an unitless duration, a `display` transition with no `allow-discrete`, another with the behaviour longhand before the shorthand, two infinite animations (one pointing at a missing `@keyframes`), a keyframes block that animates `width`, an unused keyframes block, and a reduced-motion rule that zeroes every duration with `!important`.
```
$ python3 scripts/motion_check.py motion.css
2 errors, 9 warnings, 1 info, 1 files
motion.css:1 warn will-change-broad will-change on a broad selector promotes many layers ...
motion.css:2 warn transition-all transition on all: list the exact properties
motion.css:3 error duration-no-unit transition duration has no unit (s or ms) ...
motion.css:3 warn layout-property-animated transitions height: layout work each frame
motion.css:4 error discrete-no-allow-discrete display transition without transition-behavior: allow-discrete ...
motion.css:5 warn behavior-before-shorthand transition shorthand after transition-behavior resets it ...
motion.css:6 warn infinite-no-pause infinite animation, and its selector has no reduced-motion rule ...
motion.css:7 warn animation-name-undefined animation-name orbit has no @keyframes in the scanned files
motion.css:8 warn layout-property-animated @keyframes pulse animates width: layout work each frame
motion.css:11 warn reduced-motion-blunt-reset global !important reset in reduced motion ...
```
(Messages shortened, 2 lines omitted.) The agent asks for the durations and is told the project has no tokens and the user picks 200 ms for state changes and 300 ms for panels. It lists the transitioned properties, rewrites the height and width animations as `transform`, adds units, writes the longhand after the shorthand with `allow-discrete`, defines the `orbit` keyframes, deletes the unused block and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. It greps for `animationend` and finds no handler.
```
$ python3 scripts/motion_check.py motion.css
0 errors, 0 warnings, 0 info, 1 files
```
The scanner passing shows that the patterns it knows are gone. It does not show that the frame rate improved or that the reduced version reads well. Both go in the report as untested until measured and shown to the user.
## Templates
### Report
```
Motion report: <repo or folder>, <date>
Files: <count>. Browsers: <matrix>. Motion tokens: <file or none>.
motion_check before: N errors, N warnings, N info. After: N errors, N warnings, N info.
Changes: <file:line code old -> new> (one line each)
Durations and easing chosen by the user: <token value>
Reduced-motion decisions (user approved): <selector class reduced behaviour>
Left as is: <file:line reason>
Untested: JavaScript-started motion, browsers outside the matrix, low-end devices, measured frame rate (edit to match)
```
### Starting prompt
```
Use the css-animations-transitions skill. Scope: <paths>. Browsers: <matrix>.
Symptom: <stutter | transition does nothing | cannot fade out | no reduced motion | review>.
Motion tokens: <file or none>. Scripts that use animationend, transitionend or WAAPI: <paths or none>.
Essential motion: <list>. Edit on a branch in batches of one file and show each diff first.
```
### Per-component reduced motion
```css
.toast { animation: slide-in var(--motion-panel) ease-out; }
@media (prefers-reduced-motion: reduce) {
.toast { animation: none; } /* end state at once; the fade stays on .toast__body */
}
```
## Failure modes and anti-patterns
| Mistake | How to spot it | Do instead |
|---|---|---|
| `transition: all` | `transition-all`; unrelated properties animate when a class changes | List the properties (step 5) |
| Animating layout properties | `layout-property-animated`; stutter on slow devices | `transform` or `opacity`, or FLIP (step 4) |
| A global reduced-motion kill switch | `reduced-motion-blunt-reset`; feedback vanishes and `animationend` handlers stall | Per-selector rules and a handler check (step 3) |
| `display` transition that never animates | `discrete-no-allow-discrete`, `behavior-before-shorthand` | `allow-discrete` after the shorthand, plus `@starting-style` (step 6) |
| `will-change` everywhere | `will-change-broad` | Scope it to one element and one moment (step 4) |
## Quality bar
Run every item before handing over.
1. `motion_check.py` errors are 0, and every warning and info line is fixed or listed with a reason.
2. Every duration and easing value in the changes came from the user, and the report lists each one.
3. Every remaining animation is classified in step 3 and has a reduced-motion rule written next to it, or the report says why not.
4. Every `animationend` and `transitionend` handler has a fallback when its animation is removed, or the report lists it.
5. Every animation moved onto `transform` or `opacity` has before and after numbers, or the report says it was not measured.
6. No `will-change` remains on `*`, `html`, `body` or a tag selector.
7. The reduced version of each essential animation was shown to the user.
8. Browsers outside the user's matrix are listed as untested.
## Cost
Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.
| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 89 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 6,288 | Estimate | When the skill fires |
| C. `references/patterns.md` | 1,057 | Estimate | Step 3 for the reduced-motion code, step 4 for FLIP and `will-change`, step 6 for entry and exit, step 5 for hover patterns, step 7 for the handler check |
| C. `references/appendix.md` | 1,184 | Estimate | Only for support notes, tool notes, source notes or the rival comparison |
| Script output (typical run) | 448 | Estimate | Scanner output measured on the worked example (13 lines, 1,567 characters) |
| D. Typical run (A + B + script output) | 6,825 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 9,996 | Estimate | Large input, every branch. Above 9,000: use `head -40` and read one reference at a time |
| E. Cheaper path | 6,825 | Estimate | Run the scanner, fix only flagged lines, skip both references and the emulated check |
The three rivals I read have bodies of about 1,150 and 440 tokens, or a body that was not visible (estimates from visible text). None ships a scanner and none covers `display` transitions or checks `animationend` handlers before removing motion. This skill costs more to load, and in return the scanner finds the patterns so the agent does not read every stylesheet to look for them.
## Handoffs
`css-custom-properties.md` covers registering and typing the properties you animate and the tokens that hold durations. `dialog-and-popover.md` covers the elements that most need entry and exit motion. `keyboard-focus.md` covers focus visibility during transitions. All four read different sources, so none repeats another.
## Appendix
Reusable code is in `references/patterns.md`. Support notes, tool notes and the rival comparison are in `references/appendix.md`. Read them only when a step points there.
## Sources
Last verified 7 November 2026 for every source.
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- W3C, Understanding SC 2.3.3, Animation from Interactions (WCAG 2.2): https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
- web.dev, prefers-reduced-motion: Sometimes less movement is more (last updated 11 March 2019): https://web.dev/articles/prefers-reduced-motion
- web.dev, Stick to compositor-only properties and manage layer count (last updated 20 March 2015): https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count
- Chrome for Developers, Four new CSS features for smooth entry and exit animations (last updated 16 August 2023): https://developer.chrome.com/blog/entry-exit-animations
- Josh Comeau, An Interactive Guide to CSS Transitions: https://www.joshwcomeau.com/animation/css-transitions/
- pluginagentmarketplace, css-animations. Rival implementation: https://playbooks.com/skills/pluginagentmarketplace/custom-plugin-css/css-animations
- moderndegree, motion-animation. Rival implementation: https://skills.cat/skills/moderndegree/agent-skills/motion-animation
- msrbuilds, animate-css-skill. Rival implementation, page metadata and README only: https://github.com/msrbuilds/animate-css-skill
- Anthropic, frontend-design. Related implementation: https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md
---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/
Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
Workflow
Step 1: Run the scanner
python3 scripts/motion_check.py src | head -40Read only its output. Do not read the script. The first line holds the counts. --strict exits 1 on warnings. It skips node_modules, dist, build, .git, .next and vendor, and reads .css, .scss, .sass, .less, .html, .htm, .vue, .svelte and .astro.
| Code | Level | Meaning | Fix |
|---|---|---|---|
no-reduced-motion | error | Animation or transition code and no prefers-reduced-motion query anywhere scanned | Step 3 |
duration-no-unit | error | A non-zero duration with no s or ms: invalid | Step 5 |
discrete-no-allow-discrete | error | display or content-visibility transitioned without allow-discrete | Step 6 |
transition-all | warn | transition: all or transition-property: all | Step 5 |
layout-property-animated | warn | A layout property in a transition or in @keyframes | Step 4 |
behavior-before-shorthand | warn | transition-behavior written before the transition shorthand | Step 6 |
will-change-broad | warn | will-change on *, html, body, div, span or :root | Step 4 |
reduced-motion-blunt-reset | warn | !important duration or animation reset on a broad selector inside a reduced-motion query | Step 3 |
infinite-no-pause | warn | infinite animation whose selector has no reduced-motion rule and no animation-play-state | Step 3 |
animation-name-undefined | warn | animation-name with no @keyframes in the scanned files | Step 5 |
will-change-static | info | will-change in a normal rule | Step 4 |
keyframes-unused | info | @keyframes never referenced | Step 5 |
The scanner splits on text. A hit in a string or template is a false positive: read the line. It cannot see an @keyframes defined in a file outside the scan, which produces animation-name-undefined. It checks reduced motion at file-set level, so one query in one file silences no-reduced-motion for all files: step 3 checks each animation by hand.
Step 2: Fix invalid and inert declarations
| Finding | Why it does nothing | Fix |
|---|---|---|
duration-no-unit | opacity .3 has no unit, so the declaration is invalid | 0.3s or 300ms. Only 0 may omit the unit |
animation-name-undefined | The name has no @keyframes in scope, so nothing runs | Define it, correct the spelling, or ask where it lives |
keyframes-unused | Dead code | Report. Delete only on approval, since JavaScript can set the name |
| A transition that never starts | The property has no start value in the same rule set, or the element is inserted already in its final state | Step 6 for new elements. Otherwise declare the start value |
Step 3: Reduced motion
Sources: WCAG 2.2, Understanding SC 2.3.3, Animation from Interactions, fetched 7 November 2026; web.dev, prefers-reduced-motion, last updated 11 March 2019, fetched 7 November 2026.
| Fact | Consequence |
|---|---|
| SC 2.3.3 is Level AAA: motion animation triggered by interaction can be disabled, unless it is essential (WCAG) | Aim for it, but do not describe a site as failing AA because it lacks it |
Sufficient techniques are C39 (the prefers-reduced-motion media query) and SCR40 (the same query in JavaScript) (WCAG) | Both are covered in references/patterns.md, sections 1 and 2 |
| Colour, opacity and blur changes that do not alter perceived size, shape or position are outside the glossary definition, though an erratum removes the blur exclusion (WCAG) | A fade is usually safe to keep. A blur needs a check |
| SC 2.2.2 covers animation the page starts by itself, and one animation can fail both criteria (WCAG) | Read SC 2.2.2 for its time threshold before quoting one: it was not fetched for this skill |
reduce means minimise non-essential movement, ideally to none (web.dev) | Remove movement, keep feedback. Do not remove everything |
The *, ::before, ::after reset with !important is a workaround for user stylesheets, and it uses 1 ms because some sites rely on animationend (web.dev) | Do not ship it as the site's own fix |
| The media query cannot stop motion started by the Web Animations API (web.dev) | Check JavaScript separately (step 7) |
Classify every animation the scanner and a read of the code find:
| Class | Examples | Under reduce |
|---|---|---|
| Essential | Progress bar, indeterminate loading, the only error cue | Keep, with the movement removed or shrunk: agree the replacement with the user |
| Movement | Translate, scale, rotate, parallax, bounce, slide-in | Remove: set the end state at once |
| Non-movement | Opacity or colour fade | Keep, and shorten if the user wants |
| Autoplay loop | Infinite spin, marquee, pulsing badge | Stop, or pause with a control |
Rules:
- Write the reduced rule next to the animation it changes, inside a
@media (prefers-reduced-motion: reduce)block, selector by selector. This is the pattern atreferences/patterns.mdsection 1. For new code, the opt-in form (motion insideno-preference) is the safer default: it is a house rule, and web.dev shows both forms. - Setting
animation: nonemeansanimationendnever fires. Rungrep -rnE "animationend|transitionend|onAnimationEnd|onTransitionEnd" src. Every handler found needs a fallback that runs without the event, or the reduced rule must keep a near-zero duration for that element instead. reduced-motion-blunt-resetis fixed by replacing the global reset with per-selector rules, not by deleting it and leaving nothing.infinite-no-pause: stop the loop underreduce. If the loop is essential, ask the user for the replacement (guardrail 3).
Step 4: Move animated properties onto cheap ones
Sources: web.dev, Stick to compositor-only properties and manage layer count, last updated 20 March 2015, fetched 7 November 2026; Josh Comeau, An Interactive Guide to CSS Transitions, fetched 7 November 2026. Verify before relying on this: the web.dev article is more than ten years old, so confirm in the browser's performance panel, in the browsers the user named.
| Fact | Consequence |
|---|---|
web.dev names only transform and opacity as compositor-only | Prefer them. Replace a width, height, top or left animation with transform where the visual result is the same |
For other properties the article suggests FLIP, which remaps the change onto transform and opacity | Offer FLIP only when a layout change must animate. Sketch in references/patterns.md, section 5 |
| At 60 fps a frame is about 16.6 ms (Comeau) | A layout property animated on every frame can break that budget on a slow device |
transform moves between pixels, while margin-top rounds to whole pixels (Comeau) | A margin animation can look steppy even when it is fast |
| The article suggests about 4 to 5 ms of compositing time as a target during scrolling or transitions | Treat it as a prompt to measure, not as a pass mark |
will-change rules:
| Situation | Do |
|---|---|
On *, html, body or a tag selector | Remove. web.dev shows a * rule with will-change as its bad example |
| On a class, in a static rule | Report as info. Keep only if the user can show the first frame stutters without it |
| Needed for one animation | Add it just before the motion starts and remove it after, as in references/patterns.md, section 6 |
| "How many layers is too many?" | No source read gives a number. Open the Layers panel in browser DevTools and ask the user what the target device is |
The article names Chrome DevTools: in the Paint profiler, select a frame and open the Layers tab to see each layer and why it exists. Menu names change, so verify before relying on this in the current DevTools.
Step 5: Transitions: properties, durations and easing
| Rule | Detail |
|---|---|
| List the properties | Replace all with the exact list (Comeau: "It pays to be really precise with the properties we animate") |
| Units | s or ms on every non-zero duration |
| Define the keyframes you reference | One name, one @keyframes |
| Hover triggers | Put the trigger on the parent and animate a child, so the moved element does not slide out from under the pointer and retrigger hover (Comeau) |
| Dropdown gaps | A transition-delay on the close lets the pointer cross a gap (Comeau gives 300 ms in a demo) |
Durations and easing come from the user (guardrail 2). If the project has no motion tokens, offer these as options and say where each comes from. Neither source is a standard.
| Option | Source |
|---|---|
| 125 ms for a snappy hover-in, 450 ms for a relaxed hover-out, 250 to 750 ms in general demos | Josh Comeau, demos, fetched 7 November 2026 |
| About 150 to 300 ms for UI elements, 300 to 500 ms for page transitions | moderndegree, motion-animation skill (a rival's claim), fetched 7 November 2026 |
ease-out for elements entering, ease-in only when leaving fully offscreen or becoming invisible, ease-in-out for loops | Josh Comeau, fetched 7 November 2026 |
Put the chosen values in tokens (css-custom-properties.md, step 5) and read them with var() in transition values.
Step 6: Entry, exit and display
Source: Chrome for Developers, Four new CSS features for smooth entry and exit animations, last updated 16 August 2023 with a note dated 13 September 2024, fetched 7 November 2026. Verify before relying on this for every browser outside Chrome: the page gives Chrome versions only and no Baseline status.
| Fact | Consequence |
|---|---|
Discrete properties such as display transition only with transition-behavior: allow-discrete, set for each transitioning property | Add it to every rule that transitions display or content-visibility |
The transition shorthand resets transition-behavior to normal | Write transition-behavior after the shorthand: behavior-before-shorthand |
In a transition, display switches at the start when animating in and at the end when animating out | A fade-out keeps the element visible until the fade ends |
@starting-style gives the before-open state the browser reads when an element first renders (Chrome 117) | Use it for fade-in on insert and on [open] or :popover-open |
A note says @starting-style rules must come after the normal declarations from Chrome 130 | Put the block after the base rule |
Without overlay in the transition list, a popover or dialog snaps out of the top layer and the fade-out is not seen | Add overlay with allow-discrete for top-layer elements |
With animation-fill-mode: forwards the element stays display: none. Removing the node needs a timeout so the animation can finish | Ask before removing nodes from script |
Pattern: references/patterns.md, section 4. For focus return and light dismiss on dialogs and popovers, hand over to dialog-and-popover.md.
Step 7: JavaScript and the rest
| Check | Do |
|---|---|
| Script adds classes to start motion | Make sure the class change cannot start a movement animation under reduce: use the matchMedia pattern (references/patterns.md, section 2) |
| Script uses the Web Animations API | The CSS query does not stop it (web.dev). Gate it with matchMedia |
Script waits for animationend | Step 3, rule 2 |
| Script removes nodes after an exit | Wait for the event or a timeout equal to the duration, from the same token |
Step 8: Verify and report
- Re-run the scanner. Errors must be 0. Every warning is fixed or listed with a reason.
- Emulate reduced motion in the browser or test runner (
references/appendix.md, section 3) and load one page per fixed finding. Report any movement still visible. - Compare before and after in the browser's performance panel for each animation moved onto
transformoropacity. Report the numbers, or say it was not measured. - State what is untested: JavaScript-started motion, browsers outside the matrix, low-end devices, screen reader behaviour.
- Use the report template. Do not echo unchanged CSS back.
Worked example
An invented motion.css of 12 lines: will-change on *, transition: all, a height transition and an unitless duration, a display transition with no allow-discrete, another with the behaviour longhand before the shorthand, two infinite animations (one pointing at a missing @keyframes), a keyframes block that animates width, an unused keyframes block, and a reduced-motion rule that zeroes every duration with !important.
$ python3 scripts/motion_check.py motion.css
2 errors, 9 warnings, 1 info, 1 files
motion.css:1 warn will-change-broad will-change on a broad selector promotes many layers ...
motion.css:2 warn transition-all transition on all: list the exact properties
motion.css:3 error duration-no-unit transition duration has no unit (s or ms) ...
motion.css:3 warn layout-property-animated transitions height: layout work each frame
motion.css:4 error discrete-no-allow-discrete display transition without transition-behavior: allow-discrete ...
motion.css:5 warn behavior-before-shorthand transition shorthand after transition-behavior resets it ...
motion.css:6 warn infinite-no-pause infinite animation, and its selector has no reduced-motion rule ...
motion.css:7 warn animation-name-undefined animation-name orbit has no @keyframes in the scanned files
motion.css:8 warn layout-property-animated @keyframes pulse animates width: layout work each frame
motion.css:11 warn reduced-motion-blunt-reset global !important reset in reduced motion ...(Messages shortened, 2 lines omitted.) The agent asks for the durations and is told the project has no tokens and the user picks 200 ms for state changes and 300 ms for panels. It lists the transitioned properties, rewrites the height and width animations as transform, adds units, writes the longhand after the shorthand with allow-discrete, defines the orbit keyframes, deletes the unused block and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. It greps for animationend and finds no handler.
$ python3 scripts/motion_check.py motion.css
0 errors, 0 warnings, 0 info, 1 filesThe scanner passing shows that the patterns it knows are gone. It does not show that the frame rate improved or that the reduced version reads well. Both go in the report as untested until measured and shown to the user.
Templates
Report
Motion report: <repo or folder>, <date>
Files: <count>. Browsers: <matrix>. Motion tokens: <file or none>.
motion_check before: N errors, N warnings, N info. After: N errors, N warnings, N info.
Changes: <file:line code old -> new> (one line each)
Durations and easing chosen by the user: <token value>
Reduced-motion decisions (user approved): <selector class reduced behaviour>
Left as is: <file:line reason>
Untested: JavaScript-started motion, browsers outside the matrix, low-end devices, measured frame rate (edit to match)Starting prompt
Use the css-animations-transitions skill. Scope: <paths>. Browsers: <matrix>.
Symptom: <stutter | transition does nothing | cannot fade out | no reduced motion | review>.
Motion tokens: <file or none>. Scripts that use animationend, transitionend or WAAPI: <paths or none>.
Essential motion: <list>. Edit on a branch in batches of one file and show each diff first.Per-component reduced motion
.toast { animation: slide-in var(--motion-panel) ease-out; }
@media (prefers-reduced-motion: reduce) {
.toast { animation: none; } /* end state at once; the fade stays on .toast__body */
}Failure modes and anti-patterns
| Mistake | How to spot it | Do instead |
|---|---|---|
transition: all | transition-all; unrelated properties animate when a class changes | List the properties (step 5) |
| Animating layout properties | layout-property-animated; stutter on slow devices | transform or opacity, or FLIP (step 4) |
| A global reduced-motion kill switch | reduced-motion-blunt-reset; feedback vanishes and animationend handlers stall | Per-selector rules and a handler check (step 3) |
display transition that never animates | discrete-no-allow-discrete, behavior-before-shorthand | allow-discrete after the shorthand, plus @starting-style (step 6) |
will-change everywhere | will-change-broad | Scope it to one element and one moment (step 4) |
Quality bar
Run every item before handing over.
motion_check.pyerrors are 0, and every warning and info line is fixed or listed with a reason.- Every duration and easing value in the changes came from the user, and the report lists each one.
- Every remaining animation is classified in step 3 and has a reduced-motion rule written next to it, or the report says why not.
- Every
animationendandtransitionendhandler has a fallback when its animation is removed, or the report lists it. - Every animation moved onto
transformoropacityhas before and after numbers, or the report says it was not measured. - No
will-changeremains on*,html,bodyor a tag selector. - The reduced version of each essential animation was shown to the user.
- Browsers outside the user's matrix are listed as untested.
Cost
Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the visible text of pages I read.
| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 89 | Estimate | Every session with the skill installed |
B. SKILL.md body | 6,288 | Estimate | When the skill fires |
C. references/patterns.md | 1,057 | Estimate | Step 3 for the reduced-motion code, step 4 for FLIP and will-change, step 6 for entry and exit, step 5 for hover patterns, step 7 for the handler check |
C. references/appendix.md | 1,184 | Estimate | Only for support notes, tool notes, source notes or the rival comparison |
| Script output (typical run) | 448 | Estimate | Scanner output measured on the worked example (13 lines, 1,567 characters) |
| D. Typical run (A + B + script output) | 6,825 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 9,996 | Estimate | Large input, every branch. Above 9,000: use head -40 and read one reference at a time |
| E. Cheaper path | 6,825 | Estimate | Run the scanner, fix only flagged lines, skip both references and the emulated check |
The three rivals I read have bodies of about 1,150 and 440 tokens, or a body that was not visible (estimates from visible text). None ships a scanner and none covers display transitions or checks animationend handlers before removing motion. This skill costs more to load, and in return the scanner finds the patterns so the agent does not read every stylesheet to look for them.
Handoffs
css-custom-properties.md covers registering and typing the properties you animate and the tokens that hold durations. dialog-and-popover.md covers the elements that most need entry and exit motion. keyboard-focus.md covers focus visibility during transitions. All four read different sources, so none repeats another.
Appendix
Reusable code is in references/patterns.md. Support notes, tool notes and the rival comparison are in references/appendix.md. Read them only when a step points there.
Sources
Last verified 7 November 2026 for every source.
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- W3C, Understanding SC 2.3.3, Animation from Interactions (WCAG 2.2): https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
- web.dev, prefers-reduced-motion: Sometimes less movement is more (last updated 11 March 2019): https://web.dev/articles/prefers-reduced-motion
- web.dev, Stick to compositor-only properties and manage layer count (last updated 20 March 2015): https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count
- Chrome for Developers, Four new CSS features for smooth entry and exit animations (last updated 16 August 2023): https://developer.chrome.com/blog/entry-exit-animations
- Josh Comeau, An Interactive Guide to CSS Transitions: https://www.joshwcomeau.com/animation/css-transitions/
- pluginagentmarketplace, css-animations. Rival implementation: https://playbooks.com/skills/pluginagentmarketplace/custom-plugin-css/css-animations
- moderndegree, motion-animation. Rival implementation: https://skills.cat/skills/moderndegree/agent-skills/motion-animation
- msrbuilds, animate-css-skill. Rival implementation, page metadata and README only: https://github.com/msrbuilds/animate-css-skill
- Anthropic, frontend-design. Related implementation: https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md
What this skill does
Motion code fails in ways that are easy to miss. A duration written as .3 has no unit, so the declaration is invalid. A global animation-duration: 0.01ms !important reset removes feedback people need and stalls any script waiting for animationend. A display transition does nothing without allow-discrete. A will-change rule on * is the bad example in the web.dev compositing article.
The skill runs scripts/motion_check.py, which reports file, line and code for 12 patterns, and the agent edits only the flagged lines in batches you approve. It classifies each animation as essential, movement, fade or loop, writes a reduced-motion rule next to each one, moves layout animations onto transform where the result is the same, and sets up entry and exit with @starting-style. It asks you for every duration and easing value instead of choosing one.
Who it is for
Use it if you maintain a stylesheet full of transition: all, review AI-generated animation, or ship a product with no reduced-motion handling. It fits when an animation stutters, a transition does nothing, or an element cannot fade out because it is display: none. It does not cover GSAP, Framer Motion, the Web Animations API, scroll-driven animation or SVG, and it is not a WCAG conformance statement.
What is inside
The file is 4,135 words (version 1.0.0) with one script and two reference files. It holds:
- Seven guardrails, including never choosing a duration, showing every reduced version to the user, and no global kill switch as the whole fix.
- Seven inputs the agent must have first, including which scripts listen for
animationend. - Eight workflow steps: scan, inert declarations, reduced motion, cheap properties, transitions, entry and exit, JavaScript, verify.
- One tool,
scripts/motion_check.py, a standard-library scanner with 12 codes that prints file, line and a fixed message but never source text. - A worked example with real output, a report template, a starting prompt, five failure modes and an eight-item quality bar.
| Item | Tokens | Status | Read when |
|---|---|---|---|
| Description | 89 | Estimate | Every session with the skill installed |
SKILL.md body | 6,288 | Estimate | When the skill fires |
references/patterns.md | 1,057 | Estimate | Steps 3 to 7, for reduced-motion code, hover patterns, entry and exit, FLIP, will-change or the handler check |
references/appendix.md | 1,184 | Estimate | Only for support notes, tool notes, source notes or the rival comparison |
| Typical run | 6,825 | Estimate | Description, body and scanner output |
| Worst case | 9,996 | Estimate | Both references and 40 lines of output |
| Cheaper path | 6,825 | Estimate | Run the scanner, fix flagged lines, skip the references |
The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The three rivals I read have bodies of about 1,150 and 440 tokens or a body I could not see, and none ships a scanner.
How to install and use it in Claude, Cursor and other agents
Claude. Keep the folder as css-animations-transitions/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.
Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. The scanner needs Python 3 and nothing else. Then send the starting prompt from the Templates section.
Before you install. Read the script first. It reads files and writes nothing. The skill tells the agent to edit only on a branch in batches you approve, to take every duration and easing value from you, and to show you each reduced-motion version before applying it. Install skills only from sources you trust.
A worked example
An invented 12-line motion.css has will-change on *, transition: all, a height transition with an unitless duration, two display transitions set up wrongly, two infinite animations (one pointing at missing keyframes), a keyframes block that animates width, an unused keyframes block and a reduced-motion rule that zeroes every duration with !important. The scanner prints 2 errors, 9 warnings and 1 info line.
The agent asks for the durations and is told 200 ms for state changes and 300 ms for panels. It lists the properties, rewrites the height and width animations as transform, adds units, puts allow-discrete after the shorthand, defines the missing keyframes and replaces the global reset with per-selector rules that stop both loops and keep the colour fades. The re-run prints 0 errors, 0 warnings and 0 info. Measured frame rate and the reduced version’s feel are listed as untested.
Common mistakes
The skill is built around five ways this goes wrong:
- Using transition: all. Unrelated properties animate when a class changes. The skill lists the exact properties.
- Animating layout properties. Width, height and margin force layout work each frame. The skill moves them onto transform or opacity, or uses FLIP.
- A global reduced-motion kill switch. Feedback vanishes and animationend handlers stall. The skill writes per-selector rules and greps for handlers.
- A display transition with no allow-discrete. It never animates. The skill adds the longhand after the shorthand, plus starting-style.
- will-change on everything. It promotes many layers. The skill scopes it to one element and one moment.
Related skills
This skill sits in Skills-md, Frontend Development and CSS and Layout. CSS Custom Properties covers registering the properties you animate and the tokens that hold durations. Dialog and Popover covers the elements that most need entry and exit motion. Keyboard Focus covers focus visibility during transitions. CLS Optimisation covers layout shift in Core Web Vitals.
Frequently asked questions
How do I respect prefers-reduced-motion in CSS?
Wrap movement in @media (prefers-reduced-motion: reduce) and set the end state, or put motion inside no-preference so unsupported browsers get none. Reduce means minimise non-essential movement, per web.dev, so keep fades and feedback. WCAG SC 2.3.3 is Level AAA. The skill writes the rule next to each animation and checks animationend handlers.
Why is transition: all bad?
It animates every property that changes, including ones you never meant to, and it hides what the transition costs. Josh Comeau advises listing the exact properties. The scanner flags transition: all and transition-property: all, and the skill asks you for the list, the duration and the easing instead of choosing them.
Which CSS properties are cheapest to animate?
Per web.dev, transform and opacity are the only compositor-only properties, though that article was last updated in March 2015, so confirm in your browser’s performance panel. Layout properties such as width, height, top and margin force layout work each frame. The skill flags them and offers transform or FLIP.
How do I animate display none with CSS?
Add transition-behavior: allow-discrete after the transition shorthand, list display in the transition, and give the entering state with @starting-style, per Chrome for Developers. Add overlay for dialogs and popovers so they stay in the top layer. The page gives Chrome versions only, so verify support in other browsers.
Why does my CSS transition do nothing?
Common causes are a duration with no unit, such as opacity .3, which is invalid, a property with no start value, an element inserted already in its final state, or display transitioned without allow-discrete. The scanner reports the unit and display cases by file and line, and the skill covers the rest.
Is animation-duration: 0.01ms !important a good reduced motion reset?
No. web.dev calls that reset a workaround for user stylesheets and says it uses 1 ms because some sites rely on animationend. It also removes feedback users need. The skill replaces it with per-selector rules and greps for animationend and transitionend handlers before any animation is removed.
How many tokens does the CSS Animations and Transitions skill use?
The description is about 89 tokens and loads in every session. The SKILL.md body is about 6,288 tokens when the skill fires. A typical run is about 6,825 tokens with scanner output, and the worst case with both references is about 9,996. All figures are estimates, not from a model tokeniser.
Changelog
- 1.0.0, 7 November 2026: first release.
Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
