npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

saccade

v0.13.0

Published

Slow down time to inspect animations and interactions

Downloads

1,887

Readme

saccade

npm version

Saccade is a drop-in animation inspector for web apps. Slow down time, record interactions (or just the elements you scope), scrub through transitions frame-by-frame, annotate moments, and copy structured animation state for AI coding agents.

Install

npm install saccade -D

Usage

import { Saccade } from 'saccade'

function App() {
  return (
    <>
      <YourApp />
      <Saccade />
    </>
  )
}

A floating panel appears in the corner. Use it to control speed, record, and scrub.

Slowing every animation library

Saccade slows anything driven by the standard time sources — CSS transitions, CSS @keyframes, the Web Animations API, requestAnimationFrame loops, video and audio, SVG SMIL (<animate>/<animateTransform>/<animateMotion>), and JS libraries that read Date.now/performance.now/rAF such as Framer Motion. To reach them all reliably you need two things:

1. Install before your app code (recommended)

Libraries that cache a time function before saccade patches it never see slow-mo. Because <Saccade> mounts inside React, its patches go in after anything imported earlier. Import the side-effect entry first in your app entry so the timing APIs are patched before any other module runs:

// main.tsx — must be the first import
import 'saccade/install'

import { createRoot } from 'react-dom/client'
import { App } from './App'
// …

This patches the same shared engine <Saccade> uses, so the panel still controls everything. It's optional — without it, anything imported before <Saccade> mounts may not slow down.

2. Register GSAP (ES-module imports only)

GSAP is auto-detected when loaded as a global (UMD/CDN). When you import it as an ES module, window.gsap is undefined, so hand saccade your instance once:

import { gsap } from 'gsap'
import { getSharedEngine } from 'saccade'

getSharedEngine().registerGSAP(gsap)

Features

  • Speed control — Slow down or speed up all animations: CSS transitions, CSS @keyframes, the Web Animations API, requestAnimationFrame, JS timers, video/audio, SVG SMIL, GSAP, and Framer Motion
  • Timeline recording — Record interactions, then scrub through captured frames to inspect mid-transition states. Playing <video>/<audio> is paused while you scrub (video pixels aren't DOM-replayable) and resumes from where it stopped when you clear the timeline; SMIL timelines are seeked exactly
  • Element-scoped recording — Hold S and click elements to build a scope selection (tap S or the panel's scope button to latch the picker; Esc drops it). Pick several elements — pause the page first to catch a fast-moving one — then hit record: only the selection is captured, and on scrub everything else stays frozen. Hover a scoped element for the × badge to remove it. An extension option restores the legacy pick-records-immediately behavior
  • Annotations⇧A while scrubbing turns the cursor into a picker over the frozen page: click an element mid-animation, type a note, and it pins to that element at that playhead moment as a numbered marker (hover for the note, click to edit, drafts auto-save if you click away). Annotations ride the LLM export with each element's animation state at its own timestamp — copying hands them off and clears them (setting-gated), the markers flying into the copy button
  • Page-load recordingAlt/⌥ Option+click the record button (or press ⇧⌥R) to reload with recording live from document_start: the page's entrance choreography is captured from the first paintable frame at your current speed setting, with DCL/load/LCP/fonts shown as timeline markers. Stops like any take (stop button, ⇧Space, or the 60s cap)
  • LLM export — Copy structured animation state (properties, keyframes, easing, progress) as markdown for AI coding agents
  • Shadow DOM isolation — Panel styles never leak into your app
  • Auto-stop — Recording caps at 60s (plus a frame-count memory bound) and stops when the tab is hidden for more than 5s. The take is kept, and a toast beside the panel names what ended it
  • Zero config — Drop in the component and go
  • Bug reports — Hold Alt/⌥ Option while hovering the panel to reveal a bug button. It copies a diagnostic bundle (engine state, recent internal events, environment) written for your AI agent — paste it into your agent and ask it to file the issue. Nothing is ever sent anywhere automatically.

Keyboard shortcuts

Active whenever the panel is mounted (never while typing in an input). Every hotkey takes Shift — plain keys belong to the page, so recording interactions that use Space (switches, checkboxes, buttons) or Esc (dismissing dialogs) never ends the take. The one plain-key exception is S (scope), a hold/tap gesture guarded against text entry and browser chords:

| Key | Action | |-----|--------| | ⇧R | Start/stop recording | | ⇧⌥R | Record page load — reloads with recording live from document_start (same as +clicking record) | | ⇧Space | Stop recording (while recording); otherwise pause/play — freezes time, or plays a video take while scrubbing | | ⇧S | Cycle speed | | ⇧V | Toggle video capture mode — pixel-truth takes for content DOM replay can't reach (canvas, WebGL, cross-origin iframes, <video>). If the page contains a visible cross-origin iframe when you start a DOM recording, the panel suggests this once per page load | | ⇧C | Copy the LLM export (while scrubbing) | | ⇧D | Cycle export detail level (while scrubbing) | | ⇧A | Toggle annotate mode (while scrubbing) | | S | Scope picker (while idle): hold for a momentary pick session, tap to latch it on/off; Esc drops an active latch | | ⇧Esc | Stop recording / clear the timeline |

How it works

Saccade patches timing APIs (setTimeout, setInterval, requestAnimationFrame, performance.now, Date.now) to scale time by a configurable factor — this covers requestAnimationFrame loops and time-reading libraries like Framer Motion. CSS transitions, @keyframes, and Web Animations API animations are scaled via their playbackRate; GSAP via its global timeline timeScale; SVG SMIL has no playback rate, so its timelines are paused and pumped forward by the virtual clock each frame. During recording, it snapshots computed styles, attributes, and animation state every frame. In scrub mode, it replays those snapshots by applying inline styles directly to the DOM (SMIL timelines are seeked natively via setCurrentTime). Element-scoped takes gate capture by geometry — an element belongs to the scope when its center sits inside a scoped element's live-tracked box (portalled dropdowns triggered from the scope are captured through a separate channel).

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | position | 'top-left' \| 'top-center' \| 'top-right' \| 'left-middle' \| 'right-middle' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right' | 'bottom-left' | Panel position (the panel is also draggable — it snaps to the nearest slot) | | engine | SaccadeEngine | shared singleton | Engine the panel drives. Defaults to the process-wide shared engine, so app code (via getSharedEngine()) and the panel control the same instance. Pass your own only to isolate. |

Core API

For non-React usage or programmatic control:

import { SaccadeEngine } from 'saccade/core'

const engine = new SaccadeEngine()

// Speed control
engine.setSpeed(0.25) // quarter speed
engine.getSpeed()

// Patch timing APIs now, without changing speed (win the early-load race).
// setSpeed/startRecording also install on demand, so this is only needed up front.
engine.install()

// Register a module-imported GSAP instance (window.gsap fallback otherwise)
engine.registerGSAP(gsap)

// Recording
engine.startRecording()
// …or scoped to one or more elements (everything else stays frozen in replay)
engine.startRecording(null, [document.querySelector('.hero')!])
const capture = engine.stopRecording()

// Scrubbing
engine.seekTo(500) // seek to 500ms
engine.release()

// Annotations (while scrubbing)
engine.addAnnotation(el, 'ease this out slower', 500)
engine.annotations // readonly records
engine.clearAnnotations()

// Export
const markdown = engine.exportForLLM(500, 'active', 'moderate')

// Behavior settings (extension options set these for you)
engine.setScopeAutoRecord(true) // scope pick starts recording immediately (default false)
engine.setClearAnnotationsOnExport(false) // keep annotations after copying (default true)

// Cleanup
engine.destroy()

Shared engine

new SaccadeEngine() gives you an isolated engine. If you also render <Saccade> and want app code to drive the same one the panel does, use the process-wide singleton instead:

import { getSharedEngine } from 'saccade' // or 'saccade/core'

const engine = getSharedEngine() // same instance <Saccade> uses by default
engine.setSpeed(0.5)

saccade/install installs this shared engine, so importing it and calling getSharedEngine() always refer to the same instance.

React Hooks

import { SaccadeProvider, useSaccadeEngine, useTimeline, useSpeed } from 'saccade'

useSpeed

const { speed, isPaused, setSpeed, togglePause } = useSpeed()

useTimeline

const {
  state,          // 'idle' | 'recording' | 'scrubbing'
  capture,        // TimelineCapture | null
  scrubTime,      // current scrub position in ms
  startRecording,
  stopRecording,
  seek,
  release,
  exportLLM,
} = useTimeline()

Export Detail Levels

The LLM export supports four detail levels:

| Level | Description | |-------|-------------| | brief | One line per element — property names and timing only | | moderate | Full property values, ranges, interaction state | | detailed | Adds CSS variables, @keyframes source, transition conflicts | | granular | Adds full structural element paths and environment (viewport, URL, user agent, device pixel ratio) |

Types

import type {
  AnimationInfo,
  TimelineCapture,
  TimelineExport,
  FrameSnapshot,
  ExportFilter,
  OutputDetailLevel,
} from 'saccade'

Requirements

  • React 18+ (for the <Saccade> component)
  • No React dependency needed for saccade/core

License

MIT