Skip to content

css-scroll-driven/scroll-a11y-audit

Repository files navigation

scroll-a11y-audit

An accessibility auditor for scroll-driven animations and view transitions — the two CSS features that made large-surface motion cheap enough to put on every page, and that no general-purpose accessibility checker currently understands.

It ships as two front ends over one rule engine:

  • a dependency-free browser script you paste into the console or run as a bookmarklet, which mounts a draggable, keyboard-accessible panel over the page you are looking at;
  • a Node CLI that runs the identical rules in headless Chrome, with exit codes suitable for CI.

Not published to npm or any store. There is no npm install scroll-a11y-audit and there never will be. Clone the repository and run it from source — see Installation.


Contents


Why this exists

animation-timeline: scroll() and document.startViewTransition() made a category of motion trivially easy to ship: parallax layers, reveal-on-scroll cards, full-page cross-fades. That motion is also the specific category most likely to hurt people. Large-surface movement, unexpected zoom and rotation provoke nausea, dizziness and migraine in people with vestibular disorders, and the most common implementation pattern — start at opacity: 0, let the animation bring it in — makes content unreadable the moment the animation does not run.

Existing tools do not catch this. They evaluate the DOM as it stands at one instant, which is precisely when a scroll-driven animation looks fine. They have no model of what a keyframe will do to an element, no notion of whether a prefers-reduced-motion block actually wins the cascade, and no threshold for how far is too far to move something.

This tool reads the CSS as a model — keyframes, timelines, media conditions, cascade order — and reasons about what the motion will do.

Installation

Clone it and install the single dependency:

git clone https://github.com/css-scroll-driven/scroll-a11y-audit.git
cd scroll-a11y-audit
npm install

Requirements:

  • Node 20 or newer.
  • Chrome or Chromium, for the CLI only. puppeteer-core does not download a browser; the tool drives the one you already have. See Chrome detection.

The unit tests need neither Chrome nor a network connection.

Quick start

Audit a page from the command line:

node bin/scroll-a11y-audit.mjs --url https://example.com

Try it against the bundled demo pages, one built badly on purpose and one built correctly:

python3 -m http.server 8000 &
node bin/scroll-a11y-audit.mjs \
  --url http://localhost:8000/demo/bad.html \
  --url http://localhost:8000/demo/good.html

Build the browser script and paste it into a DevTools console:

node scripts/build-browser.mjs   # → browser/scroll-a11y-audit.js

What it checks, and why it matters

The ten rules group into four concerns.

Can the user turn the motion off? prefers-reduced-motion is an operating system setting that a substantial number of people rely on, and honouring it is not optional. Three rules cover it: whether a guard exists at all (no-reduced-motion-guard), whether the guard actually works rather than merely shortening the animation (reduced-motion-still-animates), and whether view transitions — which no CSS guard touches by default — have their own opt-out (missing-view-transition-reduced-motion). The third is the one teams miss most often, because startViewTransition() never consults the preference on its own.

Is the content readable without the motion? The reveal-on-scroll pattern puts opacity: 0 in the base rule and restores it from a keyframe. When the animation does not run — reduced motion, an older browser, a failed script, a print stylesheet — the content is simply gone. content-only-revealed-by-animation catches the pattern statically and confirms it against computed style. focus-order-breakage catches its sibling problem: the invisible content is still in the tab order, so keyboard users tab into a void.

Is the motion itself safe? motion-exceeds-safe-thresholds measures what the keyframes actually do — translation as a share of the viewport, scale factor, rotation, and oscillation frequency as a flashing proxy — against configurable limits drawn from vestibular-safe design guidance. no-pause-mechanism implements WCAG 2.2.2 directly: motion that auto-plays, runs past five seconds and loops needs a control.

Does the motion break the page? animation-blocks-interaction finds view-transition layers and animated overlays that keep hit-testing while they play, swallowing clicks aimed at the content beneath. scroll-hijacking finds the patterns that take scrolling away from the user entirely.

animation-without-motion-preference-scaling is informational: it notes when a page has heavy motion but offers no in-page way to dial it down, only the all-or-nothing OS switch.

Rules reference

# Rule id Severity WCAG SC Triggers when How to fix Learn more
1 no-reduced-motion-guard critical 2.3.3 Animation from Interactions (AAA) A scroll-driven animation or @view-transition at-rule exists and no @media (prefers-reduced-motion: reduce) block neutralises it, either by name or through a blanket reset Add a reduced-motion block setting animation: none and animation-timeline: none for the selector, or a universal * reset Respecting prefers-reduced-motion in CSS
2 content-only-revealed-by-animation critical 1.4.13 / 2.3.3 The base rule resolves to opacity: 0 or visibility: hidden and only a keyframe restores visibility; or a scroll-driven element computes to zero opacity at audit time Make the resting state the readable state — animate transform only, or restore opacity: 1 inside the reduced-motion block Implementing prefers-reduced-motion
3 reduced-motion-still-animates critical 2.3.3 Animation from Interactions (AAA) A reduced-motion block exists but only shortens the duration, or is beaten in the cascade by a later rule of equal or greater specificity that re-declares the animation Set animation: none, not a shorter duration; place the block after the rules it overrides and match their specificity WCAG 2.3.3 animation-from-interactions checklist
4 focus-order-breakage serious 2.4.3 Focus Order (A), 2.4.7 Focus Visible (AA) A focusable element inside an animated container computes to zero opacity, visibility: hidden, a zero-sized box, or sits far outside the viewport Use visibility: hidden or inert — which remove elements from the tab order — instead of opacity: 0 alone, and clear them once revealed WCAG 2.2 animation compliance
5 motion-exceeds-safe-thresholds serious 2.3.3 (AAA), 2.3.1 Three Flashes (A) Keyframe travel exceeds parallaxViewportPercent of the viewport, scale exceeds maxScaleFactor, rotation exceeds maxRotationDegrees, or opacity oscillates faster than maxOscillationHz Keep travel under a fifth of the viewport, scale near 1.1×, avoid rotating large surfaces, never oscillate above ~3 Hz Designing parallax safe for vestibular disorders
6 no-pause-mechanism serious 2.2.2 Pause, Stop, Hide (A) A time-driven animation auto-plays, loops, exceeds loopDurationSeconds in total, and no pause/stop control is found anywhere on the page Add a keyboard-operable control that sets animation-play-state: paused; one site-wide toggle covers every animation it governs Pause, Stop, Hide for scroll animations
7 animation-blocks-interaction serious 2.1.1 Keyboard (A), 2.5.3 context An animated ::view-transition-* layer or full-viewport overlay lacks pointer-events: none, or a transform shifts an interactive container by more than 100 px Set pointer-events: none on transition layers and decorative overlays; animate wrappers that hold no interactive content Accessibility and inclusive motion standards
8 missing-view-transition-reduced-motion serious 2.3.3 Animation from Interactions (AAA) @view-transition, ::view-transition-* styles or startViewTransition() are used with no reduced-motion variant in CSS and no matchMedia guard in script Gate the script call on matchMedia('(prefers-reduced-motion: reduce)') and add a reduced-motion block setting animation: none on the pseudo-elements Detecting prefers-reduced-motion with matchMedia
9 scroll-hijacking moderate 2.1.1 Keyboard (A), 2.4.3 Focus Order (A) scroll-snap-type: … mandatory over content taller than snapContentHeightPx, a wheel/touchmove listener calling preventDefault(), or scroll-behavior: smooth / overscroll-behavior: none forced on the root Prefer proximity snapping, never cancel wheel events for layout, and scope smooth scrolling to prefers-reduced-motion: no-preference Vestibular-safe motion design
10 animation-without-motion-preference-scaling info Best practice, supports 2.3.3 and 2.2.2 Heavy motion is present and the page exposes no motion-intensity control — no --motion-* custom property, no labelled slider or toggle Expose an off/subtle/full preference writing a --motion-scale custom property, seeded from the media query Motion-intensity toggle with CSS custom properties

How it works internally

The design constraint driving everything: one rule engine, two runtimes. A rule written once must produce identical findings whether it runs inside a page or inside a headless Chrome driven by Node. Rules therefore never touch a global — every capability arrives through an injected context object.

flowchart TD
    subgraph shared["Shared rule engine — src/"]
        CTX["Injected context<br/>{ document, window, getComputedStyle,<br/>stylesheets?, scripts?, config? }"]
        CSS["css-model.js<br/>normalise CSSOM → plain objects<br/>flatten rules, keyframes, conditions"]
        ANIM["animation-model.js<br/>parse timelines, durations, keyframes<br/>measure translate / scale / rotate"]
        RULES["rules/*.js<br/>ten pure run(ctx, model) functions"]
        ENG["engine.js<br/>filter → run → collect → score"]
        CTX --> CSS --> ANIM --> RULES --> ENG
    end

    subgraph browser["Browser runtime"]
        BUILD["scripts/build-browser.mjs<br/>concat + strip ESM → IIFE"]
        PANEL["browser/overlay.js<br/>shadow-root panel, filters,<br/>highlighting, copy-as-Markdown"]
    end

    subgraph node["Node runtime"]
        CLI["bin/scroll-a11y-audit.mjs<br/>parseArgs, config, Chrome detection"]
        PPT["puppeteer-core → your Chrome<br/>page.evaluate(engine source)"]
        REP["reporters/*.js<br/>pretty · json · markdown · html"]
    end

    shared --> BUILD --> PANEL
    shared --> CLI --> PPT --> REP
    BUILD -.->|"same bundle, overlay entry stripped"| PPT
Loading

The injected context

Every rule receives exactly this shape. Anything absent is derived or degrades to an empty result rather than throwing.

Key Type Purpose
document Document or stub Element queries. Needs only querySelectorAll, plus documentElement and styleSheets when stylesheets is not supplied
window object innerWidth and innerHeight resolve viewport-relative units; location.href labels the result
getComputedStyle (element) => style Returns anything with getPropertyValue. Lets the tests assert on computed values without a layout engine
stylesheets array, optional Pre-normalised sheets. When omitted, they are collected from document.styleSheets
scripts string[], optional Inline script sources. When omitted, gathered from <script> elements
config object, optional Merged over the defaults by mergeConfig

Because stylesheets and scripts can be handed in directly, the entire test suite runs against a ~200-line hand-written DOM stub. There is no jsdom, and no test needs a browser.

The CSS model

collectStylesheets walks the live CSSOM once and flattens it into plain objects — style, media, supports, keyframes and view-transition rules — recording each rule's enclosing at-rule conditions and its cascade order. From that, buildAnimationModel produces the single structure every rule reads: which rules animate, which are scroll-driven, which sit inside a reduced-motion query, what each animation's keyframes actually do to translation, scale, rotation and opacity, and how those measurements compare to the viewport.

That work happens once per audit, not once per rule.

The browser build

scripts/build-browser.mjs is about 120 lines and has no dependencies. The engine's module graph is a straight line, so the build concatenates the sources in dependency order, strips the import statements and export keywords, and wraps the result in an IIFE.

This only works because every top-level identifier across src/ is unique. The build enforces that invariant itself: it collects declared names as it goes and throws on a collision, naming both files. A refactor that reintroduces a duplicate run function fails the build rather than producing a subtly broken bundle.

The CLI reuses the same artefact. It reads the built bundle, strips the IIFE wrapper and the overlay's auto-mount call, and evaluates the engine inside the page — so the CLI and the bookmarklet are running byte-identical rule code.

Scoring

Every audit produces an integer score from 0 to 100 and a letter grade.

1. Each finding carries a penalty from its severity:
      critical 20 · serious 12 · moderate 6 · minor 2 · info 0

2. Repeat findings from the same rule are damped — the nth finding of a
   given rule counts for weight / n:
      penalty(rule) = Σ  weight / n   for n = 1, 2, 3, …

3. Each rule's total is capped:
      penalty(rule) = min(penalty(rule), 34)

4. score = round(max(0, 100 − Σ penalty(rule)))

Steps 2 and 3 exist because one misconfigured selector can match forty elements. Without damping, a single rule firing repeatedly would drown out forty genuinely distinct problems and every imperfect page would score zero, which tells you nothing. With it, breadth of failure moves the score more than depth: one critical issue lands at 80 (grade B), four distinct criticals land in the F band.

Score Grade Reading
95–100 A+ No findings, or informational only
90–94 A Minor polish outstanding
85–89 B+ One moderate issue
80–84 B One serious or critical issue
70–79 C Several issues; likely a real barrier for some users
60–69 D Multiple serious failures
0–59 F Broad failure across rules

A score is a triage aid, not a compliance certificate. Read the findings.

CLI reference

node bin/scroll-a11y-audit.mjs --url <url> [options]
Flag Short Default Description
--url <url> -u Page to audit. Repeat for several pages. Required
--reporter <name> -r pretty pretty, json, markdown or html
--out <file> -o stdout Write the report to a file. Disables ANSI colour
--fail-on <severity> Exit 1 when a finding of this severity or worse exists
--rules <ids> all Comma-separated allow-list of rule ids
--exclude-rules <ids> none Comma-separated deny-list of rule ids
--config <file> -c JSON configuration file
--viewport <WxH> 1280x800 Viewport size; also resolves vh/vw units in keyframes
--wait-for <selector> Wait for a CSS selector before auditing
--timeout <ms> 30000 Navigation and wait timeout
--chrome-path <path> auto Chrome executable. Also read from CHROME_PATH
--headful off Run Chrome with a visible window
--verbose -v off Add remediation text and links to the pretty reporter
--list-rules Print the rule catalogue and exit
--help -h Show usage
--version Print the version

Exit codes

Code Meaning
0 The audit ran and --fail-on was not tripped
1 A finding met or exceeded --fail-on
2 Invalid usage — unknown flag, rule, reporter, severity or viewport
3 Runtime error — Chrome missing, navigation failed, bundle not built

Chrome detection

puppeteer-core never downloads a browser. The CLI resolves one in this order:

  1. --chrome-path
  2. the CHROME_PATH environment variable
  3. the usual install locations for Linux, macOS and Windows — including /usr/bin/google-chrome, /usr/bin/chromium, /snap/bin/chromium, /opt/google/chrome/chrome, the two macOS .app bundles and both Windows Program Files paths

If none is executable, the CLI exits 3 and lists every path it probed.

Examples

# JSON to a file
node bin/scroll-a11y-audit.mjs --url https://example.com -r json -o report.json

# Fail CI on anything serious or worse
node bin/scroll-a11y-audit.mjs --url https://example.com --fail-on serious

# Only the reduced-motion rules, on a mobile viewport
node bin/scroll-a11y-audit.mjs --url https://example.com \
  --rules no-reduced-motion-guard,reduced-motion-still-animates \
  --viewport 390x844

# Wait for a client-rendered app to settle, watch it happen
node bin/scroll-a11y-audit.mjs --url https://example.com \
  --wait-for "[data-hydrated]" --headful --verbose

Configuration

Pass a JSON file with --config, or set window.__SCROLL_A11Y_AUDIT_CONFIG__ before the browser script runs. Both use the same schema, and both merge over the defaults — you only specify what you are changing. See scroll-a11y.config.example.json.

{
  "rules": { "animation-without-motion-preference-scaling": false },
  "severityOverrides": { "scroll-hijacking": "serious" },
  "thresholds": { "parallaxViewportPercent": 20, "maxScaleFactor": 1.2 }
}

Schema

Key Type Default Meaning
rules Record<string, boolean> {} Set a rule id to false to skip it. Unlisted rules run
severityOverrides Record<string, severity> {} Remap a rule's severity. Must be one of critical, serious, moderate, minor, info — anything else throws
thresholds object see below Numeric tuning. A non-numeric value throws

Thresholds

Threshold Default Unit Used by Meaning
parallaxViewportPercent 15 % of viewport rule 5, 10 Maximum translation as a share of the viewport's relevant axis
maxScaleFactor 1.5 multiplier rule 5, 10 Maximum scale reached by any keyframe
maxRotationDegrees 15 degrees rule 5 Maximum absolute rotation in an animation
loopDurationSeconds 5 seconds rule 6 Total run time past which a looping animation needs a pause control. The WCAG 2.2.2 value
maxOscillationHz 3 hertz rule 5 Direction reversals per second before oscillation counts as flashing
snapContentHeightPx 3000 pixels rule 9 Scrollable height above which mandatory snapping is flagged
invisibleOpacity 0.05 0–1 rules 2, 4, 5 Opacity at or below which content counts as invisible

Validation is strict and fails at startup: an unknown severity or a non-numeric threshold exits 2 with a message naming the offending key, rather than silently auditing with the wrong settings.

Browser and bookmarklet usage

Build the script, then paste it into a DevTools console or wire it to a bookmark. Full instructions, including CSP and HTTPS caveats, are in browser/bookmarklet.md.

node scripts/build-browser.mjs

The panel docks to the top-right: a header reading scroll-a11y-audit with a colour-coded score chip (green at 90+, amber from 70, red below) and a close button. Under it sits a row of severity pills — critical (3), serious (5), moderate (2) — that toggle their groups on and off. The list below holds one card per finding, striped down the left edge in its severity colour, each showing a severity badge, the rule title, the element's selector path in monospace, the WCAG criterion, a plain-language explanation, a bolded Fix: line, the offending CSS in a scrollable code block, and a "Learn more" link. A footer pins Copy as Markdown and Log JSON in place.

Hovering or tabbing to a card paints a translucent, severity-coloured rectangle over the offending element in the page behind the panel.

The panel is itself accessible — it would be embarrassing otherwise. It is a role="dialog" in a shadow root, draggable by mouse and by focusing the header and pressing the arrow keys, every control is keyboard-operable, filter state is announced through aria-pressed and a live region, and Escape closes it. The audit runs before the panel is inserted, so the panel never audits itself.

Sample reports

demo/good.html — accessible by construction

scroll-a11y-audit — http://localhost:8000/demo/good.html
────────────────────────────────────────────────────────────────────────
Score  100/100   Grade  A+
Issues none

  No accessibility problems detected in the page's scroll-driven or view-transition motion.

That page uses the same visual ideas as the bad one — reveal-on-scroll cards, a parallax layer, view transitions — built correctly: opaque resting states, travel measured in tens of pixels, pointer-events: none on transition layers, proximity snapping, a matchMedia guard around startViewTransition(), a --motion-scale custom property wired to a three-state control, and a blanket reduced-motion reset placed last in the sheet.

demo/bad.html — every rule triggered

scroll-a11y-audit — http://localhost:8000/demo/bad.html
────────────────────────────────────────────────────────────────────────
Score  0/100   Grade  F
Issues 8 critical · 18 serious · 3 moderate · 1 info

CRITICAL Content is only reachable through an animation
  rule     content-only-revealed-by-animation
  element  .reveal-card
  css      .reveal-card { opacity: 0; animation-fill-mode: both; animation-timeline: view(); … }
  wcag     WCAG 1.4.13 / 2.3.3 — content must not depend on motion to be perceivable
  why      The base style resolves to an invisible state (opacity 0) and only
           "fade-up" restores it. If the animation never runs — reduced motion,
           an unsupported browser, a JavaScript error, or print — the content
           stays hidden.

CRITICAL Reduced-motion override does not actually stop the motion
  rule     reduced-motion-still-animates
  element  .marquee-banner
  css      .marquee-banner { animation-duration: 3s; }
  wcag     WCAG 2.3.3 Animation from Interactions (AAA)
  why      The reduced-motion override only shortens the animation to 3s.
           Shortened motion is still motion — the animation continues to play
           for users who asked for none.

SERIOUS  Motion exceeds vestibular-safe thresholds
  rule     motion-exceeds-safe-thresholds
  element  .parallax-layer
  css      .parallax-layer { animation: drift linear; animation-timeline: scroll(root block); }
  wcag     WCAG 2.3.3 Animation from Interactions (AAA), 2.3.1 Three Flashes (A)
  why      This animation moves content 90% of the viewport (limit 15%), about
           720px at the audited viewport size; scales by 2.6× (limit 1.5×).

  … 27 more findings

Both reports above are real output from the CLI against the bundled demo pages.

Continuous integration

The CLI is designed to gate a build. --fail-on sets the severity that turns a report into a failure; everything below it is reported but does not block.

# .github/workflows/motion-a11y.yml
name: Motion accessibility

on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install
        run: npm install

      - name: Build the rule bundle
        run: node scripts/build-browser.mjs

      - name: Serve the site
        run: |
          npx --yes http-server ./dist -p 8080 &
          npx --yes wait-on http://localhost:8080

      - name: Audit
        env:
          # ubuntu-latest ships Chrome at this path
          CHROME_PATH: /usr/bin/google-chrome
        run: |
          node bin/scroll-a11y-audit.mjs \
            --url http://localhost:8080/ \
            --url http://localhost:8080/pricing \
            --reporter markdown \
            --out motion-a11y.md \
            --fail-on serious

      - name: Publish to the job summary
        if: always()
        run: cat motion-a11y.md >> "$GITHUB_STEP_SUMMARY"

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: motion-a11y-report
          path: motion-a11y.md

Two practical notes. Start with --fail-on critical on an existing codebase and tighten to serious once the backlog is clear — a gate that fails on day one gets disabled on day two. And keep the --reporter markdown output flowing into the job summary even when the gate passes, so the trend stays visible.

Limitations and false positives

This is static analysis of CSS plus one computed-style snapshot. It is genuinely useful and it is genuinely incomplete. Be honest with yourself about the gap.

What it cannot see:

  • Cross-origin stylesheets. The CSSOM throws on cssRules for a sheet served from another origin without CORS headers. Those sheets are skipped and a warning names them in the report. A page whose motion lives entirely in a third-party stylesheet may audit clean while being badly broken.
  • External scripts. Only inline <script> bodies are read. A startViewTransition() call or a preventDefault() wheel handler in a bundled file will be missed, so rules 8 and 9 under-report on most real applications.
  • Animation driven from JavaScript. The Web Animations API, GSAP, Framer Motion and friends never touch CSS, so nothing here sees them.
  • One moment in time. The audit runs at the initial scroll position. Content revealed by later scrolling, route changes or interaction is not visited. Use --wait-for, and audit several URLs.
  • Whether the motion is actually a problem. No tool can tell you that a parallax effect is essential to the design, or that a hidden card is decorative.

Where it over-reports, and what to do:

  • content-only-revealed-by-animation flags any opacity: 0 base rule with an animated reveal. If a reduced-motion block restores opacity by a route the selector matcher does not connect, this is a false positive. Verify by emulating reduced motion in DevTools and reloading.
  • focus-order-breakage measures the live DOM at one scroll position, so content legitimately below the fold can look "far offscreen". Findings whose rect is merely below the viewport deserve a manual look before you act.
  • motion-exceeds-safe-thresholds resolves vh/vw/% units against the audited viewport. A different --viewport gives different numbers. Tune parallaxViewportPercent to your design language rather than suppressing the rule.
  • no-pause-mechanism finds pause controls by matching label text against a word list (pause, stop, motion toggle, and similar) and by data-motion-toggle. A control in another language, or one labelled only with an icon, will not be recognised. Add data-motion-toggle to it.
  • animation-blocks-interaction flags transforms over 100 px containing focusable elements. On a carousel that is expected behaviour, not a bug.
  • no-reduced-motion-guard matches guard selectors to animated selectors with a deliberately cheap heuristic. Complex selector architectures will produce misses in both directions; a blanket * reset is both better practice and reliably detected.

Above all: a passing audit is not a passing page. Nothing here replaces testing with reduced motion enabled at the OS level, tabbing through the page with the animations running, and asking someone with a vestibular disorder what they experience. Use the score to find work, not to declare it finished.

Development

npm install                          # one dependency: puppeteer-core
npm test                             # node --test, no framework, no Chrome needed
node scripts/build-browser.mjs       # rebuild the IIFE
node --check browser/scroll-a11y-audit.js   # verify the bundle parses

Layout:

src/
  constants.js         severities, weights, grade bands
  config.js            defaults, merging, validation, rule filtering
  css-model.js         CSSOM → plain objects; flattening; specificity
  animation-model.js   timelines, durations, keyframe measurement
  engine.js            orchestration and scoring
  rules/               ten rules plus the registry
  reporters/           pretty, json, markdown, html, ANSI helpers
bin/                   the CLI
browser/               overlay source, built bundle, bookmarklet guide
scripts/               the hand-rolled browser build
demo/                  bad.html and good.html
tests/                 unit and integration tests, plus the DOM stub

The test suite covers each of the ten rules in both directions, the scoring formula and its damping, config merging and validation, the CSS and animation parsers, the rule registry's metadata, and all four reporters including HTML escaping. It runs in well under a second.

Contributing

Bug reports, and especially false-positive reports with a reproducing test case, are the most valuable contributions. See CONTRIBUTING.md for the ground rules — chiefly that puppeteer-core stays the only dependency, that rules never touch globals, and that every rule ships with tests in both directions.

Further reading

Background on the standards and techniques these rules encode:

Licence

MIT © 2026 css-scroll-driven.

Maintained alongside css-scroll-driven.com, a reference site for CSS scroll-driven animations.

About

Accessibility auditor for scroll-driven and view-transition animations — 10 WCAG-mapped rules, as a Node CLI and a browser bookmarklet.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors