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