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.

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.

Raw SKILL.md file

.md

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.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.

#InputAsk
1Paths to scan"Which folders hold the CSS, components and templates?"
2Goal"Fix existing animations, or write a new one? If new: progress bar, reveal or parallax?" New work: read references/patterns.md.
3Browser 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.
4Fallback 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.
5Stack"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.
6Edit permission"Branch name, and do you approve edits in batches of up to 30?"
7A 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.

CodeMeaningHandled in
S01animation-timeline with no @supports guardStep 2
S02animation shorthand declared after animation-timeline in the same ruleStep 3
S03infinite iteration on a scroll timelineStep 4
S04Fill mode is not bothStep 4
S05Named timeline used but never definedStep 5
S06Named timelines used, no timeline-scope anywhereStep 5
S07No prefers-reduced-motion rule in any scanned fileStep 6
S08Keyframes used by a scroll timeline animate a layout propertyStep 7
S09ScrollTimeline or ViewTimeline used with no feature checkStep 8
S10overflow that may make an ancestor the nearest scrollerStep 8
S11animation-range with no range nameStep 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.

FloorAction
Includes a browser that lacks the feature, or you could not verify oneWrap 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 todayThe guard is optional. Remove S01 findings from scope with the user's approval and record the floor in the report.
User cannot sayKeep 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:
IntentRange
Reveal while the element entersentry 0% entry 100% (spans 0% of cover to 0% of contain)
Leave animationexit 0% exit 100%
Run for the whole time it is visiblecover 0% cover 100%
Run only while fully inside the scrollportcontain 0% contain 100%
Reading progress, whole pageanimation-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:

AnimationUnder reduce
Parallax, zoom, rotation, large translate, anything that moves content against the scrollanimation: 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 placeMay 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. 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:

.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

FailureHow to spot itDo instead
Shorthand below animation-timelineThe animation runs on time, or never scrolls; S02Step 3: shorthand first, longhands after
Timeline attached to a box that cannot scrollNothing animates, no error; S10Step 8: find the nearest scroller, use overflow: clip or name the right one
Named timeline on a siblingThe name resolves to nothing; S05 or S06Step 5: timeline-scope on a common ancestor
Reduced motion ignored, or used to hide contentS07; the page is blank with the setting onStep 6: remove motion, keep the content
Support claims copied from an old postA 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.

ItemTokensRead when
Description85Every session with the skill installed
This body4,225When the skill fires
references/support.md814Version, Baseline or source questions only
references/patterns.md479Writing a new animation only
references/example.md807Only to see full files and output
Typical run4,710Description, body, 25 lines of output
Worst case7,370All 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):

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.
ItemTokensStatusRead when
Description85EstimateEvery session with the skill installed
SKILL.md body4,225EstimateWhen the skill fires
references/support.md814EstimateOnly for version, polyfill or source questions
references/patterns.md479EstimateOnly when writing a new animation
references/example.md807EstimateOnly to see the full example files
Typical run4,710EstimateDescription, body and 25 lines of scanner output
Worst case7,370EstimateAll references and 60 lines of output
Cheaper path4,710EstimateRun 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-scope on 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.

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

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.

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.

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.

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.

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.

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.

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.