CSS and Layout skill 11 of 12
CSS Scroll-Driven Animations SKILL.md: Fix animation-timeline Bugs
The CSS Scroll-Driven Animations skill makes Claude, Cursor or another AI agent find why an animation-timeline does nothing, and fix it. A scanner reports each fault by file and line, the agent edits only flagged lines, and you approve each batch. It asks for your browser support floor instead of choosing one.
css-scroll-driven-animations/SKILL.md, version 1.0.0, 3,060 words, plus scripts/sda_check.py and three 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.
Free to use and adapt with credit to [Afeez Adebayo](https://afeezadebayo.com) ([LinkedIn](https://www.linkedin.com/in/afeez-adebayo/)). Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-scroll-driven-animations/SKILL.md
In this skill
- When to use it
- Guardrails
- Inputs
- Workflow (steps 1 to 9)
- Worked example
- Templates
- Failure modes
- Quality bar
- Cost
- Handoffs, appendix, sources
1. When to use it
Use it when a stylesheet or component uses, or is about to use, animation-timeline, scroll(), view(), scroll-timeline, view-timeline, timeline-scope, animation-range, or the ScrollTimeline and ViewTimeline JavaScript classes, and one of these is true: an animation does nothing, it plays on load instead of on scroll, it jumps, it runs in one browser only, or the code is AI-generated and unreviewed. Use it to write a new reading progress bar, scroll reveal or parallax that is correct on the first pass.
Do not use it for GSAP, ScrollTrigger or other JavaScript animation libraries, View Transitions, @starting-style entry animations, IntersectionObserver reveals, or smooth scrolling. For transition: all, layout animation and display fades use [css-animations-transitions](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/). Hand those back or to that skill.
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as css-scroll-driven-animations/SKILL.md.
---
name: css-scroll-driven-animations
description: "Audits and fixes CSS scroll-driven animations (animation-timeline, scroll(), view(), animation-range). Runs a scanner, then edits flagged lines: shorthand order resets, fill mode, infinite iterations, named timeline scope, nearest-scroller overflow, layout-property keyframes, missing prefers-reduced-motion and support guards. Use for 'animation-timeline not working', 'scroll animation', 'view() reveal', 'reading progress bar', 'CSS parallax'. Do NOT use for GSAP or ScrollTrigger, View Transitions, IntersectionObserver reveals or @starting-style."
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-scroll-driven-animations/
category: frontend-development
subcategory: css-layout
version: "1.0.0"
created: "2026-11-07"
updated: "2026-11-07"
---
# CSS Scroll-Driven Animations
Free to use and adapt with credit to [Afeez Adebayo](https://afeezadebayo.com) ([LinkedIn](https://www.linkedin.com/in/afeez-adebayo/)). Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-scroll-driven-animations/SKILL.md
## In this skill
1. When to use it
2. Guardrails
3. Inputs
4. Workflow (steps 1 to 9)
5. Worked example
6. Templates
7. Failure modes
8. Quality bar
9. Cost
10. Handoffs, appendix, sources
## 1. When to use it
Use it when a stylesheet or component uses, or is about to use, `animation-timeline`, `scroll()`, `view()`, `scroll-timeline`, `view-timeline`, `timeline-scope`, `animation-range`, or the `ScrollTimeline` and `ViewTimeline` JavaScript classes, and one of these is true: an animation does nothing, it plays on load instead of on scroll, it jumps, it runs in one browser only, or the code is AI-generated and unreviewed. Use it to write a new reading progress bar, scroll reveal or parallax that is correct on the first pass.
Do not use it for GSAP, ScrollTrigger or other JavaScript animation libraries, View Transitions, `@starting-style` entry animations, IntersectionObserver reveals, or smooth scrolling. For `transition: all`, layout animation and `display` fades use [css-animations-transitions](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/). Hand those back or to that skill.
## 2. Guardrails
- Edits are bulk edits. Work on a branch, write nothing until the user approves a batch, and never edit `main`. Default to a report. The scanner never writes files.
- Never choose a browser support floor. Ask for it (Input 3). Support changed during 2026, and posts and skills written a few months ago are wrong now. Verify on MDN, web.dev or caniuse before relying on any version number. The body of this skill carries none; `references/support.md` carries dated ones.
- Never add a polyfill or a new dependency without approval. A polyfill is third-party code that runs on every page.
- Data touched: source files only, matched by extension (`.css .scss .sass .less .html .vue .svelte .astro .php .mdx .js .jsx .ts .tsx .mjs`). The scanner prints file, line, code and a fixed message, never other source text. Do not paste whole files, `.env` files, tokens or customer text into the prompt. It skips `node_modules`, build output and minified files.
- Motion is an accessibility matter. Scroll-linked motion can trigger vestibular symptoms. Do not remove a reduced-motion rule to make a design match. This is not an accessibility conformance statement; a person must test with the setting on.
- Edit only flagged lines. Do not reformat, reorder or rewrite whole files, and do not echo unchanged code back.
## 3. Inputs
Ask for anything missing, in one message. Do not assume.
| # | Input | Ask |
|---|---|---|
| 1 | Paths to scan | "Which folders hold the CSS, components and templates?" |
| 2 | Goal | "Fix existing animations, or write a new one? If new: progress bar, reveal or parallax?" New work: read `references/patterns.md`. |
| 3 | Browser support floor | "Which browsers and versions must show the scroll effect, and which only need the page to work?" If the answer includes a browser that may lack the feature, stop and tell the user to check MDN or caniuse. Do not guess a version. |
| 4 | Fallback behaviour | "In a browser without support, should content appear at once, play a timed animation, or use a JavaScript fallback?" Default to appear at once. |
| 5 | Stack | "Plain CSS, Sass, CSS-in-JS, Tailwind, a component framework?" The scanner reads style blocks in `.vue`, `.svelte`, `.astro` and `.html` files, not CSS-in-JS template strings. Read those by hand. |
| 6 | Edit permission | "Branch name, and do you approve edits in batches of up to 30?" |
| 7 | A way to see it | "Dev server URL, or a browser tool?" If none, mark every visual check untested. |
## 4. Workflow
Run the script from the skill folder. Read its output only. Do not read the script or redo its checks by hand.
### Step 1: summary scan
```
python3 scripts/sda_check.py <paths> --summary
```
Exit 0: report "no scroll-driven animation faults found" and stop. Exit 1: continue. Exit 2: fix the arguments.
| Code | Meaning | Handled in |
|---|---|---|
| S01 | `animation-timeline` with no `@supports` guard | Step 2 |
| S02 | `animation` shorthand declared after `animation-timeline` in the same rule | Step 3 |
| S03 | `infinite` iteration on a scroll timeline | Step 4 |
| S04 | Fill mode is not `both` | Step 4 |
| S05 | Named timeline used but never defined | Step 5 |
| S06 | Named timelines used, no `timeline-scope` anywhere | Step 5 |
| S07 | No `prefers-reduced-motion` rule in any scanned file | Step 6 |
| S08 | Keyframes used by a scroll timeline animate a layout property | Step 7 |
| S09 | `ScrollTimeline` or `ViewTimeline` used with no feature check | Step 8 |
| S10 | `overflow` that may make an ancestor the nearest scroller | Step 8 |
| S11 | `animation-range` with no range name | Step 4 |
S01, S06, S10 and S11 are review codes. The scanner cannot see the DOM, so each needs a decision, not an automatic edit.
### Step 2: support strategy (S01)
Use the floor from Input 3 and the fallback from Input 4.
| Floor | Action |
|---|---|
| Includes a browser that lacks the feature, or you could not verify one | Wrap each scroll-driven rule in `@supports (animation-timeline: view())` (or `scroll()`), and make the unguarded default correct: content visible, no hidden start state. |
| Every required browser supports it, confirmed on MDN or caniuse today | The guard is optional. Remove S01 findings from scope with the user's approval and record the floor in the report. |
| User cannot say | Keep the guard. It costs three lines per block. |
A browser that does not understand `animation-timeline` drops that one declaration. The animation then runs on the document timeline, so check what that looks like: a reveal that plays once on load is acceptable, one that starts at `opacity: 0` and waits is not.
### Step 3: shorthand order (S02)
The `animation` shorthand resets `animation-timeline` and `animation-range`. The CSS Working Group treats them as reset-only sub-properties of the shorthand, and WebKit closed the request to set a timeline inside the shorthand as WONTFIX in September 2024. Move the shorthand above the longhands in the same rule. Do not move a longhand into the shorthand.
### Step 4: duration, iteration, fill mode, range (S03, S04, S11)
- Duration: leave `animation-duration` unset, or `auto`, which is the default. The module divides the attached range by the iteration count to get the used duration. Do not add a time value, and remove one only when it sits in a flagged shorthand, because what a time value does on a scroll timeline differs by implementation and this skill has not verified it.
- S03: replace `infinite` with `1`, or delete it. The spec gives an infinite count a used duration of zero, so nothing visibly animates.
- S04: add `both` (shorthand) or `animation-fill-mode: both`. Without it the element shows its base style before and after the range, which is a visible jump when the first or last keyframe differs from the base.
- S11: name the range. Percentages without a name measure the whole timeline. Choose from this table:
| Intent | Range |
|---|---|
| Reveal while the element enters | `entry 0% entry 100%` (spans 0% of `cover` to 0% of `contain`) |
| Leave animation | `exit 0% exit 100%` |
| Run for the whole time it is visible | `cover 0% cover 100%` |
| Run only while fully inside the scrollport | `contain 0% contain 100%` |
| Reading progress, whole page | `animation-timeline: scroll(root)` with no range |
The ranges above are defined in the CSSWG Scroll-driven Animations module, section 3.1. Check the spec if a sticky element is involved: for sticky boxes a 0% or 100% point can match many scroll positions.
### Step 5: named timelines (S05, S06)
A name resolves by walking up from the element that references it. Two outcomes:
- Defined on an ancestor of the animated element: it works with no `timeline-scope`.
- Defined on a sibling, cousin or descendant: add `timeline-scope: --name` on a common ancestor.
One element can define only one timeline per name. When names collide, the later declaration wins, and a scroll timeline beats a view timeline. Rename one. For S05, find the definition (`view-timeline-name`, `scroll-timeline-name`, or the `view-timeline` and `scroll-timeline` shorthands) or fix the typo. Dashed idents are case-sensitive.
### Step 6: reduced motion (S07)
Browsers do not force reduced motion on scroll-linked animations. The CSSWG decided in 2022 that `prefers-reduced-motion` stays the mechanism and developers write the reduced version, because both forced options (no animation, nearest keyframe) can hide content. Apply this rule:
| Animation | Under `reduce` |
|---|---|
| Parallax, zoom, rotation, large translate, anything that moves content against the scroll | `animation: none` |
| Reveal (opacity, short translate) | `animation: none`, and set the revealed state as the default so content is visible |
| Progress bar, colour or opacity change in place | May stay |
```css
@media (prefers-reduced-motion: reduce) {
.reveal, .parallax { animation: none; }
}
```
Put it after the animation rules, in the same layer. Never use the reduce block to hide content.
### Step 7: keyframes (S08)
Change layout properties in the keyframes to `transform` or `opacity`: `height` becomes `scaleY` with a `transform-origin`, `margin-left` becomes `translate`, `width` becomes `scaleX`. Declarative scroll-driven animations can run off the main thread, per the scroll-driven-animations.style demo site, and layout properties take that away. If the design needs real layout change, say so in the report and keep it.
### Step 8: scroller and JavaScript (S09, S10)
- S10: for each flagged `overflow`, ask whether it wraps an animated element. If yes, that box is the nearest scroller. When it cannot scroll, the timeline is inactive, because the spec makes a timeline inactive when its source is not a scroll container or has no scrollable overflow. Change `overflow: hidden` to `overflow: clip` where the box should not scroll (verify support), or move the timeline to the real scroller with `scroll(<scroller> <axis>)` or a named timeline.
- S09: wrap `new ScrollTimeline` and `new ViewTimeline` in `if ('ScrollTimeline' in window)`. Offer a fallback per Input 4. Pass `fill: 'both'` to `animate()`.
- A scroller that is the page uses `scroll(root)`. A scroller that is the nearest overflow ancestor uses `scroll()` or `scroll(nearest)`. Axis defaults to `block`; use `inline` for horizontal scrollers.
### Step 9: verify, then report
1. Re-run `python3 scripts/sda_check.py <paths> --summary`. Every remaining code appears in the report with an action or a reason.
2. In a browser that supports the feature, scroll the page and check the start and end of each animation by eye. If no browser tool exists, write "untested".
3. Emulate `prefers-reduced-motion: reduce` and confirm content is still visible.
4. If the floor includes a browser without support, load the page there, or with the `@supports` block disabled, and confirm the default is acceptable.
5. Use the report template (Templates). One row per file and code.
## 5. Worked example
Input: an invented `page.css` (10 rules) and `app.js`; `references/example.md` holds both files and the full output. Floor given by the user: current Chrome and Safari must show the effect, other browsers must show content. Scan:
```
$ python3 scripts/sda_check.py page.css app.js --summary
app.js S09x1
page.css S01x4 S02x1 S03x1 S04x3 S05x1 S06x1 S07x1 S08x2 S10x1 S11x1
2 files scanned: 17 findings S01x4 S02x1 S03x1 S04x3 S05x1 S06x1 S07x1 S08x2 S09x1 S10x1 S11x1
```
Step 2: unguarded blocks are wrapped in `@supports`. Step 3: in `.progress`, `animation: grow linear` sat below `animation-timeline: scroll(root block)`, so the timeline was reset and the bar grew over the default time; the shorthand moves up. Step 4: `.reveal` loses `infinite` and the `1s` time value, gains `both`, and `animation-range: 0% 40%` becomes `entry 0% entry 100%`. Step 5: `--hero` is used in `.hero img` and defined nowhere, so the rule defines `view-timeline-name: --hero` on `.hero`. Step 6: a `reduce` block sets `animation: none` on `.reveal` and `.card`. Step 7: `slide` moves from `margin-left` to `translate`, and `expand` moves from `height` to `scaleY`. Step 8: `.page-wrap { overflow: hidden }` wraps `.reveal`, so it becomes `overflow: clip`; `app.js` gets an `in window` check. The re-run prints `no scroll-driven animation faults found`. The visual checks stay untested unless someone scrolls the page in a supporting browser.
## 6. Templates
Starting prompt:
```
Use the css-scroll-driven-animations skill on <paths>. Browser floor: <floor>. Fallback: <appear at once | timed | JS>. Work on branch <name>. Run the scanner first and show me the summary. Do not edit until I approve a batch.
```
Report:
```
Scanned: <n> files. Before: <count by code>. After: <count by code>.
| File | Code | Action | Reason if left |
Floor used: <the user's>. Fallback: <chosen>.
Scrolled in a supporting browser: <checked | untested>
Reduced motion emulated: <checked | untested>
Unsupported browser or guard off: <checked | untested>
Needs a human: motion comfort testing with real users who are sensitive to motion.
```
Guarded reveal, the shape most fixes take:
```css
.reveal { opacity: 1; }
@supports (animation-timeline: view()) {
.reveal {
animation: fade-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; } }
@keyframes fade-up { from { opacity: 0; translate: 0 2rem; } }
```
## 7. Failure modes
| Failure | How to spot it | Do instead |
|---|---|---|
| Shorthand below `animation-timeline` | The animation runs on time, or never scrolls; S02 | Step 3: shorthand first, longhands after |
| Timeline attached to a box that cannot scroll | Nothing animates, no error; S10 | Step 8: find the nearest scroller, use `overflow: clip` or name the right one |
| Named timeline on a sibling | The name resolves to nothing; S05 or S06 | Step 5: `timeline-scope` on a common ancestor |
| Reduced motion ignored, or used to hide content | S07; the page is blank with the setting on | Step 6: remove motion, keep the content |
| Support claims copied from an old post | A guard removed because "Firefox has no support", or kept because it "needs a polyfill" | Step 2: ask for the floor, verify it today |
## 8. Quality bar
Run each check before handing over.
1. Step 9.1 re-run shows no S02, S03, S05 or S08 findings, and every other code has an action or reason in the report.
2. `git diff --stat` shows only files the scanner flagged.
3. No `animation-timeline` rule has an `animation` shorthand below it in the same rule.
4. The default (unguarded) state of every animated element is visible and usable.
5. A `prefers-reduced-motion` rule exists, sits after the animation rules and does not hide content.
6. The browser floor in the report is the user's, with the date it was verified, not yours.
7. Each of the three visual checks in step 9 is stated as checked or untested.
8. Every edit sits on a named branch and each batch was approved.
## 9. Cost
All figures are estimates (words x 1.35 for prose, characters / 3.5 for code and tables), not from a model tokeniser.
| Item | Tokens | Read when |
|---|---|---|
| Description | 85 | Every session with the skill installed |
| This body | 4,225 | When the skill fires |
| `references/support.md` | 814 | Version, Baseline or source questions only |
| `references/patterns.md` | 479 | Writing a new animation only |
| `references/example.md` | 807 | Only to see full files and output |
| Typical run | 4,710 | Description, body, 25 lines of output |
| Worst case | 7,370 | All references and 60 lines of output |
Cheaper path: run `--summary`, approve one batch, edit only flagged lines, re-run, skip every reference. On the worked example the summary prints about 51 tokens, the full per-line output about 425 tokens, and the fixture files are about 340 tokens. On files this small the full output costs more than reading them. The scanner earns its cost as the codebase grows, because summary output is one line per file however long the file is. These are measured by character count on the fixtures; they are not a saving on your code.
## 10. Handoffs, appendix, sources
Handoffs: [css-animations-transitions](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/) for transitions and `display` fades, [css-custom-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-custom-properties/) for animating registered properties, [css-anchor-positioning](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-anchor-positioning/) for the sibling feature, [colour-contrast](https://afeezadebayo.com/skills-md/frontend-development/accessibility/colour-contrast/) when a fade changes legibility.
Tool appendix (outside the tool-agnostic core): `references/support.md` holds dated support notes, polyfill notes and the rival comparison; `references/patterns.md` holds new-animation patterns.
Sources (last verified 7 November 2026):
- W3C CSSWG, CSS Scroll-driven Animations Module Level 1 (Editor's Draft): https://drafts.csswg.org/scroll-animations-1/
- MDN, CSS scroll-driven animations: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations
- web.dev, New to the web platform in October, published 29 October 2026: https://web.dev/blog/web-platform-10-2026
- Bramus, Scroll-driven Animations demos and tools: https://scroll-driven-animations.style/
- WebKit Bugzilla 279732, animation-timeline in the shorthand (RESOLVED WONTFIX, 24 September 2024): https://bugs.webkit.org/show_bug.cgi?id=279732
- CSSWG, TAG feedback on prefers-reduced-motion (issue 5321): https://lists.w3.org/Archives/Public/public-css-archive/2022Jun/0886.html
- Anthropic, Agent Skills best practices (body under 500 lines, 1,024 character description): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Paul Irish, modern-css skill: https://github.com/paulirish/dotfiles/blob/main/agents/skills/modern-css/SKILL.md
- AThevon, css-native skill: https://github.com/AThevon/genjutsu
- yonatangross, scroll-driven-animations skill (OrchestKit): https://skillselion.com/skills/yonatangross/orchestkit/scroll-driven-animations
---
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-scroll-driven-animations/
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.
2. Guardrails
- Edits are bulk edits. Work on a branch, write nothing until the user approves a batch, and never edit
main. Default to a report. The scanner never writes files. - Never choose a browser support floor. Ask for it (Input 3). Support changed during 2026, and posts and skills written a few months ago are wrong now. Verify on MDN, web.dev or caniuse before relying on any version number. The body of this skill carries none;
references/support.mdcarries dated ones. - Never add a polyfill or a new dependency without approval. A polyfill is third-party code that runs on every page.
- Data touched: source files only, matched by extension (
.css .scss .sass .less .html .vue .svelte .astro .php .mdx .js .jsx .ts .tsx .mjs). The scanner prints file, line, code and a fixed message, never other source text. Do not paste whole files,.envfiles, tokens or customer text into the prompt. It skipsnode_modules, build output and minified files. - Motion is an accessibility matter. Scroll-linked motion can trigger vestibular symptoms. Do not remove a reduced-motion rule to make a design match. This is not an accessibility conformance statement; a person must test with the setting on.
- Edit only flagged lines. Do not reformat, reorder or rewrite whole files, and do not echo unchanged code back.
3. Inputs
Ask for anything missing, in one message. Do not assume.
| # | Input | Ask |
|---|---|---|
| 1 | Paths to scan | "Which folders hold the CSS, components and templates?" |
| 2 | Goal | "Fix existing animations, or write a new one? If new: progress bar, reveal or parallax?" New work: read references/patterns.md. |
| 3 | Browser support floor | "Which browsers and versions must show the scroll effect, and which only need the page to work?" If the answer includes a browser that may lack the feature, stop and tell the user to check MDN or caniuse. Do not guess a version. |
| 4 | Fallback behaviour | "In a browser without support, should content appear at once, play a timed animation, or use a JavaScript fallback?" Default to appear at once. |
| 5 | Stack | "Plain CSS, Sass, CSS-in-JS, Tailwind, a component framework?" The scanner reads style blocks in .vue, .svelte, .astro and .html files, not CSS-in-JS template strings. Read those by hand. |
| 6 | Edit permission | "Branch name, and do you approve edits in batches of up to 30?" |
| 7 | A way to see it | "Dev server URL, or a browser tool?" If none, mark every visual check untested. |
4. Workflow
Run the script from the skill folder. Read its output only. Do not read the script or redo its checks by hand.
Step 1: summary scan
python3 scripts/sda_check.py <paths> --summaryExit 0: report "no scroll-driven animation faults found" and stop. Exit 1: continue. Exit 2: fix the arguments.
| Code | Meaning | Handled in |
|---|---|---|
| S01 | animation-timeline with no @supports guard | Step 2 |
| S02 | animation shorthand declared after animation-timeline in the same rule | Step 3 |
| S03 | infinite iteration on a scroll timeline | Step 4 |
| S04 | Fill mode is not both | Step 4 |
| S05 | Named timeline used but never defined | Step 5 |
| S06 | Named timelines used, no timeline-scope anywhere | Step 5 |
| S07 | No prefers-reduced-motion rule in any scanned file | Step 6 |
| S08 | Keyframes used by a scroll timeline animate a layout property | Step 7 |
| S09 | ScrollTimeline or ViewTimeline used with no feature check | Step 8 |
| S10 | overflow that may make an ancestor the nearest scroller | Step 8 |
| S11 | animation-range with no range name | Step 4 |
S01, S06, S10 and S11 are review codes. The scanner cannot see the DOM, so each needs a decision, not an automatic edit.
Step 2: support strategy (S01)
Use the floor from Input 3 and the fallback from Input 4.
| Floor | Action |
|---|---|
| Includes a browser that lacks the feature, or you could not verify one | Wrap each scroll-driven rule in @supports (animation-timeline: view()) (or scroll()), and make the unguarded default correct: content visible, no hidden start state. |
| Every required browser supports it, confirmed on MDN or caniuse today | The guard is optional. Remove S01 findings from scope with the user's approval and record the floor in the report. |
| User cannot say | Keep the guard. It costs three lines per block. |
A browser that does not understand animation-timeline drops that one declaration. The animation then runs on the document timeline, so check what that looks like: a reveal that plays once on load is acceptable, one that starts at opacity: 0 and waits is not.
Step 3: shorthand order (S02)
The animation shorthand resets animation-timeline and animation-range. The CSS Working Group treats them as reset-only sub-properties of the shorthand, and WebKit closed the request to set a timeline inside the shorthand as WONTFIX in September 2024. Move the shorthand above the longhands in the same rule. Do not move a longhand into the shorthand.
Step 4: duration, iteration, fill mode, range (S03, S04, S11)
- Duration: leave
animation-durationunset, orauto, which is the default. The module divides the attached range by the iteration count to get the used duration. Do not add a time value, and remove one only when it sits in a flagged shorthand, because what a time value does on a scroll timeline differs by implementation and this skill has not verified it. - S03: replace
infinitewith1, or delete it. The spec gives an infinite count a used duration of zero, so nothing visibly animates. - S04: add
both(shorthand) oranimation-fill-mode: both. Without it the element shows its base style before and after the range, which is a visible jump when the first or last keyframe differs from the base. - S11: name the range. Percentages without a name measure the whole timeline. Choose from this table:
| Intent | Range |
|---|---|
| Reveal while the element enters | entry 0% entry 100% (spans 0% of cover to 0% of contain) |
| Leave animation | exit 0% exit 100% |
| Run for the whole time it is visible | cover 0% cover 100% |
| Run only while fully inside the scrollport | contain 0% contain 100% |
| Reading progress, whole page | animation-timeline: scroll(root) with no range |
The ranges above are defined in the CSSWG Scroll-driven Animations module, section 3.1. Check the spec if a sticky element is involved: for sticky boxes a 0% or 100% point can match many scroll positions.
Step 5: named timelines (S05, S06)
A name resolves by walking up from the element that references it. Two outcomes:
- Defined on an ancestor of the animated element: it works with no
timeline-scope. - Defined on a sibling, cousin or descendant: add
timeline-scope: --nameon a common ancestor.
One element can define only one timeline per name. When names collide, the later declaration wins, and a scroll timeline beats a view timeline. Rename one. For S05, find the definition (view-timeline-name, scroll-timeline-name, or the view-timeline and scroll-timeline shorthands) or fix the typo. Dashed idents are case-sensitive.
Step 6: reduced motion (S07)
Browsers do not force reduced motion on scroll-linked animations. The CSSWG decided in 2022 that prefers-reduced-motion stays the mechanism and developers write the reduced version, because both forced options (no animation, nearest keyframe) can hide content. Apply this rule:
| Animation | Under reduce |
|---|---|
| Parallax, zoom, rotation, large translate, anything that moves content against the scroll | animation: none |
| Reveal (opacity, short translate) | animation: none, and set the revealed state as the default so content is visible |
| Progress bar, colour or opacity change in place | May stay |
@media (prefers-reduced-motion: reduce) {
.reveal, .parallax { animation: none; }
}Put it after the animation rules, in the same layer. Never use the reduce block to hide content.
Step 7: keyframes (S08)
Change layout properties in the keyframes to transform or opacity: height becomes scaleY with a transform-origin, margin-left becomes translate, width becomes scaleX. Declarative scroll-driven animations can run off the main thread, per the scroll-driven-animations.style demo site, and layout properties take that away. If the design needs real layout change, say so in the report and keep it.
Step 8: scroller and JavaScript (S09, S10)
- S10: for each flagged
overflow, ask whether it wraps an animated element. If yes, that box is the nearest scroller. When it cannot scroll, the timeline is inactive, because the spec makes a timeline inactive when its source is not a scroll container or has no scrollable overflow. Changeoverflow: hiddentooverflow: clipwhere the box should not scroll (verify support), or move the timeline to the real scroller withscroll(<scroller> <axis>)or a named timeline. - S09: wrap
new ScrollTimelineandnew ViewTimelineinif ('ScrollTimeline' in window). Offer a fallback per Input 4. Passfill: 'both'toanimate(). - A scroller that is the page uses
scroll(root). A scroller that is the nearest overflow ancestor usesscroll()orscroll(nearest). Axis defaults toblock; useinlinefor horizontal scrollers.
Step 9: verify, then report
- Re-run
python3 scripts/sda_check.py <paths> --summary. Every remaining code appears in the report with an action or a reason. - In a browser that supports the feature, scroll the page and check the start and end of each animation by eye. If no browser tool exists, write "untested".
- Emulate
prefers-reduced-motion: reduceand confirm content is still visible. - If the floor includes a browser without support, load the page there, or with the
@supportsblock disabled, and confirm the default is acceptable. - Use the report template (Templates). One row per file and code.
5. Worked example
Input: an invented page.css (10 rules) and app.js; references/example.md holds both files and the full output. Floor given by the user: current Chrome and Safari must show the effect, other browsers must show content. Scan:
$ python3 scripts/sda_check.py page.css app.js --summary
app.js S09x1
page.css S01x4 S02x1 S03x1 S04x3 S05x1 S06x1 S07x1 S08x2 S10x1 S11x1
2 files scanned: 17 findings S01x4 S02x1 S03x1 S04x3 S05x1 S06x1 S07x1 S08x2 S09x1 S10x1 S11x1Step 2: unguarded blocks are wrapped in @supports. Step 3: in .progress, animation: grow linear sat below animation-timeline: scroll(root block), so the timeline was reset and the bar grew over the default time; the shorthand moves up. Step 4: .reveal loses infinite and the 1s time value, gains both, and animation-range: 0% 40% becomes entry 0% entry 100%. Step 5: --hero is used in .hero img and defined nowhere, so the rule defines view-timeline-name: --hero on .hero. Step 6: a reduce block sets animation: none on .reveal and .card. Step 7: slide moves from margin-left to translate, and expand moves from height to scaleY. Step 8: .page-wrap { overflow: hidden } wraps .reveal, so it becomes overflow: clip; app.js gets an in window check. The re-run prints no scroll-driven animation faults found. The visual checks stay untested unless someone scrolls the page in a supporting browser.
6. Templates
Starting prompt:
Use the css-scroll-driven-animations skill on <paths>. Browser floor: <floor>. Fallback: <appear at once | timed | JS>. Work on branch <name>. Run the scanner first and show me the summary. Do not edit until I approve a batch.Report:
Scanned: <n> files. Before: <count by code>. After: <count by code>.
| File | Code | Action | Reason if left |
Floor used: <the user's>. Fallback: <chosen>.
Scrolled in a supporting browser: <checked | untested>
Reduced motion emulated: <checked | untested>
Unsupported browser or guard off: <checked | untested>
Needs a human: motion comfort testing with real users who are sensitive to motion.Guarded reveal, the shape most fixes take:
.reveal { opacity: 1; }
@supports (animation-timeline: view()) {
.reveal {
animation: fade-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; } }
@keyframes fade-up { from { opacity: 0; translate: 0 2rem; } }7. Failure modes
| Failure | How to spot it | Do instead |
|---|---|---|
Shorthand below animation-timeline | The animation runs on time, or never scrolls; S02 | Step 3: shorthand first, longhands after |
| Timeline attached to a box that cannot scroll | Nothing animates, no error; S10 | Step 8: find the nearest scroller, use overflow: clip or name the right one |
| Named timeline on a sibling | The name resolves to nothing; S05 or S06 | Step 5: timeline-scope on a common ancestor |
| Reduced motion ignored, or used to hide content | S07; the page is blank with the setting on | Step 6: remove motion, keep the content |
| Support claims copied from an old post | A guard removed because "Firefox has no support", or kept because it "needs a polyfill" | Step 2: ask for the floor, verify it today |
8. Quality bar
Run each check before handing over.
- Step 9.1 re-run shows no S02, S03, S05 or S08 findings, and every other code has an action or reason in the report.
git diff --statshows only files the scanner flagged.- No
animation-timelinerule has ananimationshorthand below it in the same rule. - The default (unguarded) state of every animated element is visible and usable.
- A
prefers-reduced-motionrule exists, sits after the animation rules and does not hide content. - The browser floor in the report is the user's, with the date it was verified, not yours.
- Each of the three visual checks in step 9 is stated as checked or untested.
- Every edit sits on a named branch and each batch was approved.
9. Cost
All figures are estimates (words x 1.35 for prose, characters / 3.5 for code and tables), not from a model tokeniser.
| Item | Tokens | Read when |
|---|---|---|
| Description | 85 | Every session with the skill installed |
| This body | 4,225 | When the skill fires |
references/support.md | 814 | Version, Baseline or source questions only |
references/patterns.md | 479 | Writing a new animation only |
references/example.md | 807 | Only to see full files and output |
| Typical run | 4,710 | Description, body, 25 lines of output |
| Worst case | 7,370 | All references and 60 lines of output |
Cheaper path: run --summary, approve one batch, edit only flagged lines, re-run, skip every reference. On the worked example the summary prints about 51 tokens, the full per-line output about 425 tokens, and the fixture files are about 340 tokens. On files this small the full output costs more than reading them. The scanner earns its cost as the codebase grows, because summary output is one line per file however long the file is. These are measured by character count on the fixtures; they are not a saving on your code.
10. Handoffs, appendix, sources
Handoffs: [css-animations-transitions](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-animations-transitions/) for transitions and display fades, [css-custom-properties](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-custom-properties/) for animating registered properties, [css-anchor-positioning](https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-anchor-positioning/) for the sibling feature, [colour-contrast](https://afeezadebayo.com/skills-md/frontend-development/accessibility/colour-contrast/) when a fade changes legibility.
Tool appendix (outside the tool-agnostic core): references/support.md holds dated support notes, polyfill notes and the rival comparison; references/patterns.md holds new-animation patterns.
Sources (last verified 7 November 2026):
- W3C CSSWG, CSS Scroll-driven Animations Module Level 1 (Editor's Draft): https://drafts.csswg.org/scroll-animations-1/
- MDN, CSS scroll-driven animations: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations
- web.dev, New to the web platform in October, published 29 October 2026: https://web.dev/blog/web-platform-10-2026
- Bramus, Scroll-driven Animations demos and tools: https://scroll-driven-animations.style/
- WebKit Bugzilla 279732, animation-timeline in the shorthand (RESOLVED WONTFIX, 24 September 2024): https://bugs.webkit.org/show_bug.cgi?id=279732
- CSSWG, TAG feedback on prefers-reduced-motion (issue 5321): https://lists.w3.org/Archives/Public/public-css-archive/2022Jun/0886.html
- Anthropic, Agent Skills best practices (body under 500 lines, 1,024 character description): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Paul Irish, modern-css skill: https://github.com/paulirish/dotfiles/blob/main/agents/skills/modern-css/SKILL.md
- AThevon, css-native skill: https://github.com/AThevon/genjutsu
- yonatangross, scroll-driven-animations skill (OrchestKit): https://skillselion.com/skills/yonatangross/orchestkit/scroll-driven-animations
What this skill does
Scroll-driven animations tie an animation to scroll position with animation-timeline, scroll() and view(). They fail without an error. An animation shorthand written below animation-timeline resets it. A view() timeline on a box that cannot scroll never starts. A named timeline on a sibling resolves to nothing. Fill mode, an infinite count or a layout property in the keyframes each break the effect in a different way.
The skill runs scripts/sda_check.py, which reports file, line and code for 11 patterns and never edits a file. The agent fixes the flagged lines on a branch, in batches you approve, using a decision rule for each code: a support guard, shorthand order, fill mode, named ranges, timeline scope, a reduced-motion rule and keyframes that use transform and opacity. It asks for your browser floor, because support changed during 2026 and older posts are out of date. Every visual check it could not run is listed as untested.
Who it is for
Use it if a scroll reveal, reading progress bar or parallax does nothing, plays on load or works in one browser only, if you are replacing a scroll listener with CSS, or if you are reviewing AI-generated animation code. It does not cover GSAP, ScrollTrigger, View Transitions or smooth scrolling, and it is not an accessibility sign-off. A person must test with reduced motion switched on.
What is inside
The file is 3,060 words (version 1.0.0) with one script and three reference files. It holds:
- Six guardrails, including edits on a branch only, no polyfill without approval and no support floor chosen by the agent.
- Seven inputs the agent must have first, including the browser floor and what should happen without support.
- Nine workflow steps: scan, support strategy, shorthand order, duration and fill mode, named timelines, reduced motion, keyframes, scroller and JavaScript, then verify and report.
- One scanner,
scripts/sda_check.py, standard library only, with 11 codes. It prints file, line and a fixed message. - A worked example with real scanner output, a starting prompt and report template, five failure modes and an eight-item quality bar.
| Item | Tokens | Status | Read when |
|---|---|---|---|
| Description | 85 | Estimate | Every session with the skill installed |
SKILL.md body | 4,225 | Estimate | When the skill fires |
references/support.md | 814 | Estimate | Only for version, polyfill or source questions |
references/patterns.md | 479 | Estimate | Only when writing a new animation |
references/example.md | 807 | Estimate | Only to see the full example files |
| Typical run | 4,710 | Estimate | Description, body and 25 lines of scanner output |
| Worst case | 7,370 | Estimate | All references and 60 lines of output |
| Cheaper path | 4,710 | Estimate | Run the summary scan, edit flagged lines, re-run, 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 are about 4,500, 2,400 and 1,500 tokens (estimates), and none ships a scanner. On the worked example the summary scan prints about 51 tokens against about 340 for the fixture files.
How to install and use it in Claude, Cursor and other agents
Claude. Keep the folder as css-scroll-driven-animations/ 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 scanner first. It reads files and never writes. The skill tells the agent to work on a branch, to edit in batches you approve, never to add a polyfill without asking and never to choose your browser support floor. Install skills only from sources you trust.
A worked example
An invented page.css has a progress bar whose animation shorthand sits below its animation-timeline, a reveal with an infinite count and a percentage range, a hero that uses a timeline named --hero that nothing defines, keyframes that animate margin-left and height, and a wrapper with overflow: hidden. The scanner prints 17 findings across the stylesheet and a script.
The agent wraps the blocks in @supports, moves the shorthand up, removes infinite, adds both, names the range entry 0% entry 100%, defines --hero on .hero, switches the keyframes to translate and scaleY, changes the wrapper to overflow: clip and adds a reduced-motion block. The re-run prints no findings. Scrolling in a supporting browser and the reduced-motion view are listed as untested until someone checks them.
Common mistakes
The skill is built around five ways this goes wrong:
- Shorthand below animation-timeline. The timeline is reset and the animation runs on time. The scanner marks it S02.
- A timeline on a box that cannot scroll. Nothing animates and nothing warns you. The skill finds the nearest scroller.
- A named timeline on a sibling. The name resolves only by walking up, so the skill adds
timeline-scopeon a common ancestor. - Ignoring reduced motion, or hiding content to honour it. The skill removes the movement and keeps the content.
- Copying support claims from an old post. The skill asks for your floor and tells you to verify it on the day.
Related skills
This skill sits in Skills-md, Frontend Development and CSS and Layout. CSS Animations and Transitions covers transitions, transition: all and display fades. CSS Custom Properties covers animating registered properties. CSS Anchor Positioning covers tooltips and menus. Colour Contrast covers legibility when a fade changes colour.
Frequently asked questions
Why is my animation-timeline not working?
The usual causes are an animation shorthand declared after animation-timeline, which resets it, a scroller that cannot scroll, a named timeline on a sibling, an infinite iteration count, and a browser without support. The scanner reports them as S02, S10, S05 or S06, S03 and S01, each with file and line.
What is the difference between scroll() and view() in CSS?
scroll() makes a timeline from how far a scroll container has scrolled. view() makes one from how much of an element is visible in its scrollport. Both default to the nearest scroller and the block axis, according to the CSSWG module. Use view() for reveals and scroll() for progress bars.
Do CSS scroll-driven animations work in Firefox and Safari?
web.dev listed them as Baseline Newly available on 29 October 2026, with Chrome 115, Edge 115, Firefox 159 and Safari 26. Older posts say Firefox lacks support. Check MDN or caniuse on the day. The skill asks for your browser floor and does not choose one.
Do I need prefers-reduced-motion for scroll animations?
Yes. The CSSWG decided in 2022 that browsers would not force reduced motion on scroll-linked animations, because both options could hide content. Developers write the reduced version. The skill removes parallax and large movement under reduce and keeps content visible.
How do I make a scroll reveal animation in CSS?
Use animation-timeline: view() with animation-range: entry 0% entry 100% and fill mode both, inside an @supports block, with a prefers-reduced-motion rule. Put the animation shorthand before animation-timeline. The skill ships this pattern in its templates and checks each part with the scanner.
Why does my scroll animation play on page load?
A browser without support drops the animation-timeline declaration, so the animation runs on the normal time-based timeline. A shorthand below the timeline does the same in a browser that supports it. Wrap the rule in @supports and keep the shorthand above the longhands.
How many tokens does the CSS Scroll-Driven Animations skill use?
The description is about 85 tokens and loads in every session. The SKILL.md body is about 4,225 tokens when the skill fires. A typical run is about 4,710 tokens with scanner output, and the worst case with all three references is about 7,370. All figures are estimates from words and characters, 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.
