diff --git a/.changeset/animation-initial.md b/.changeset/animation-initial.md
new file mode 100644
index 000000000..27e38e3ad
--- /dev/null
+++ b/.changeset/animation-initial.md
@@ -0,0 +1,21 @@
+---
+"@solid-primitives/animation": minor
+---
+
+New package. Provides reactive and imperative wrappers for the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) (WAAPI). All primitives follow the `make*` / `create*` convention: `make*` is imperative and returns immediately, `create*` is a reactive wrapper that re-runs on dependency change and cancels on owner disposal.
+
+- `makeAnimate(el, keyframes, options?)` — thin wrapper around `element.animate()`
+- `createAnimate(target, keyframes, options?)` — reactive `makeAnimate`; re-runs whenever target, keyframes, or options change
+- `makeScrollAnimation(el, keyframes, options?)` — scroll-driven animation via `ScrollTimeline`
+- `createScrollAnimation(target, keyframes, options?)` — reactive `makeScrollAnimation`
+- `makeViewAnimation(el, keyframes, options?)` — viewport-driven animation via `ViewTimeline`; defaults `rangeStart`/`rangeEnd` to the entry phase so initially-visible elements animate correctly
+- `createViewAnimation(target, keyframes, options?)` — reactive `makeViewAnimation`
+- `makeFlip(el, options?)` — FLIP layout animation; `snapshot()` before DOM change, `flip()` after
+- `makeStagger(els, keyframes, options?)` — staggered WAAPI animation across a list of elements with per-element delay offset
+- `createStagger(targets, keyframes, options?)` — reactive `makeStagger`
+- `makeAnimationGroup(animations)` — coordinates a static list of `Animation` objects as a unit; forwards `play`, `pause`, `cancel`, `reverse`, and `finish` to all simultaneously
+- `createAnimationGroup(animations)` — reactive `makeAnimationGroup`; re-derives the group whenever the accessor returns a new list
+- `makeMotionPath(el, path, options?)` — animates an element along a CSS `offset-path` using WAAPI
+- `createMotionPath(target, path, options?)` — reactive `makeMotionPath`
+- `makeSequence(factories)` — chains animation factories into a sequential playlist; each factory is called lazily when its predecessor finishes
+- `createPresenceAnimation(target, show, options)` — manages mount/unmount lifecycle with WAAPI enter/exit animations; element stays mounted until its exit animation completes
diff --git a/README.md b/README.md
index bf1aad04a..28c593f21 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://pnpm.io/)
[](https://vitest.dev)
-[](https://dash.deno.com/playground/combined-npm-downloads)
+[](https://dash.deno.com/playground/combined-npm-downloads)
Solid Primitives is a project dedicated to building high-quality, community-contributed primitives for SolidJS. Every utility is thoroughly tested, continuously maintained, and reviewed against a consistent quality bar before it lands in the repository. Our aim is to extend Solid's primary and secondary primitives with a well-rounded set of tertiary primitives.
@@ -139,6 +139,7 @@ See the [CHANGELOG](https://github.com/solidjs-community/solid-primitives/tree/n
|[orientation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/orientation#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[makeOrientation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/orientation#makeorientation) [createOrientation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/orientation#createorientation)|[](https://bundlephobia.com/package/@solid-primitives/orientation)|[](https://www.npmjs.com/package/@solid-primitives/orientation)|✓|
|[vibrate](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[isVibrationSupported](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#isvibrationsupported) [makeVibrate](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#makevibrate) [createVibrate](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#createvibrate) [frequencyToPattern](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#frequencytopattern) [makePulse](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#makepulse) [createPulse](https://github.com/solidjs-community/solid-primitives/tree/main/packages/vibrate#createpulse)|[](https://bundlephobia.com/package/@solid-primitives/vibrate)|[](https://www.npmjs.com/package/@solid-primitives/vibrate)|✓|
|
*Animation*
|
+|[animation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createAnimate](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#createanimate) [createScrollAnimation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#createscrollanimation) [createViewAnimation](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#createviewanimation) [makeFlip](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#makeflip) [createStagger](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#createstagger) [createAnimationGroup](https://github.com/solidjs-community/solid-primitives/tree/main/packages/animation#createanimationgroup)|[](https://bundlephobia.com/package/@solid-primitives/animation)|[](https://www.npmjs.com/package/@solid-primitives/animation)||
|[presence](https://github.com/solidjs-community/solid-primitives/tree/main/packages/presence#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createPresence](https://github.com/solidjs-community/solid-primitives/tree/main/packages/presence#createpresence)|[](https://bundlephobia.com/package/@solid-primitives/presence)|[](https://www.npmjs.com/package/@solid-primitives/presence)|✓|
|[raf](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createRAF](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createraf) [createMs](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#createms) [targetFPS](https://github.com/solidjs-community/solid-primitives/tree/main/packages/raf#targetfps)|[](https://bundlephobia.com/package/@solid-primitives/raf)|[](https://www.npmjs.com/package/@solid-primitives/raf)|✓|
|[spring](https://github.com/solidjs-community/solid-primitives/tree/main/packages/spring#readme)|[](https://github.com/solidjs-community/solid-primitives/blob/main/CONTRIBUTING.md#contribution-process)|[createSpring](https://github.com/solidjs-community/solid-primitives/tree/main/packages/spring#createspring) [createDerivedSpring](https://github.com/solidjs-community/solid-primitives/tree/main/packages/spring#createderivedspring)|[](https://bundlephobia.com/package/@solid-primitives/spring)|[](https://www.npmjs.com/package/@solid-primitives/spring)|✓|
diff --git a/packages/animation/CHANGELOG.md b/packages/animation/CHANGELOG.md
new file mode 100644
index 000000000..02a02c3fd
--- /dev/null
+++ b/packages/animation/CHANGELOG.md
@@ -0,0 +1,10 @@
+# @solid-primitives/animation
+
+## 0.0.1
+
+### Minor Changes
+
+- Initial release. WAAPI-based animation primitives for SolidJS: `makeAnimate`, `createAnimate`,
+ `makeScrollAnimation`, `createScrollAnimation`, `makeViewAnimation`, `createViewAnimation`,
+ `makeFlip`, `makeStagger`, `createStagger`, `makeAnimationGroup`, `createAnimationGroup`,
+ `makeMotionPath`, `createMotionPath`, `makeSequence`, `createPresenceAnimation`.
diff --git a/packages/animation/LICENSE b/packages/animation/LICENSE
new file mode 100644
index 000000000..38b41d975
--- /dev/null
+++ b/packages/animation/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Solid Primitives Working Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/packages/animation/README.md b/packages/animation/README.md
new file mode 100644
index 000000000..742cc2784
--- /dev/null
+++ b/packages/animation/README.md
@@ -0,0 +1,357 @@
+
+
+
+
+# @solid-primitives/animation
+
+[](https://www.npmjs.com/package/@solid-primitives/animation)
+[](https://github.com/solidjs-community/solid-primitives#contribution-process)
+[](https://vitest.dev)
+
+Solid primitives for the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) (WAAPI). Each primitive follows the `make` / `create`.
+## Installation
+
+```bash
+npm install @solid-primitives/animation
+# or
+pnpm add @solid-primitives/animation
+```
+
+## Primitives
+
+| Primitive | Description |
+|---|---|
+| [`makeAnimate`](#makeanimate--createanimate) | Imperative `element.animate()` wrapper |
+| [`createAnimate`](#makeanimate--createanimate) | Reactive `makeAnimate` |
+| [`makeScrollAnimation`](#makescrollanimation--createscrollanimation) | Scroll-driven animation via `ScrollTimeline` |
+| [`createScrollAnimation`](#makescrollanimation--createscrollanimation) | Reactive `makeScrollAnimation` |
+| [`makeViewAnimation`](#makeviewanimation--createviewanimation) | Viewport-driven animation via `ViewTimeline` |
+| [`createViewAnimation`](#makeviewanimation--createviewanimation) | Reactive `makeViewAnimation` |
+| [`makeFlip`](#makeflip) | FLIP layout animation |
+| [`makeStagger`](#makestagger--createstagger) | Staggered animations across a list of elements |
+| [`createStagger`](#makestagger--createstagger) | Reactive `makeStagger` |
+| [`makeAnimationGroup`](#makeanimationgroup--createanimationgroup) | Coordinate multiple animations as a unit |
+| [`createAnimationGroup`](#makeanimationgroup--createanimationgroup) | Reactive `makeAnimationGroup` |
+| [`createPresenceAnimation`](#createpresenceanimation) | Mount/unmount lifecycle with WAAPI enter/exit animations |
+
+---
+
+## `makeAnimate` / `createAnimate`
+
+`makeAnimate` is a thin wrapper around `element.animate()` with TypeScript types. `createAnimate` replays the animation whenever `target`, `keyframes`, or `options` change reactively, and cancels it when the owner disposes.
+
+```ts
+// Imperative
+const anim = makeAnimate(el, [{ opacity: 0 }, { opacity: 1 }], { duration: 300 });
+anim.pause();
+
+// Reactive
+const anim = createAnimate(
+ () => ref,
+ [{ opacity: 0 }, { opacity: 1 }],
+ { duration: 300, fill: "forwards" },
+);
+// anim() is the current Animation instance, or undefined while ref is unset
+anim()?.pause();
+```
+
+```ts
+function makeAnimate(
+ el: Element,
+ keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
+ options?: KeyframeAnimationOptions,
+): Animation
+
+function createAnimate(
+ target: Accessor,
+ keyframes: MaybeAccessor,
+ options?: MaybeAccessor,
+): Accessor
+```
+
+---
+
+## `makeScrollAnimation` / `createScrollAnimation`
+
+Plays a WAAPI animation whose progress is driven by scroll position via [`ScrollTimeline`](https://developer.mozilla.org/en-US/docs/Web/API/ScrollTimeline). No scroll listeners or RAF loops needed.
+
+```ts
+// Fade + rise as the user scrolls down the page
+const anim = createScrollAnimation(
+ () => ref,
+ [{ opacity: 0, transform: "translateY(20px)" }, { opacity: 1, transform: "none" }],
+ { fill: "both" },
+);
+
+// Tie progress to a specific scroll container
+const anim = createScrollAnimation(() => ref, keyframes, {
+ fill: "both",
+ source: scrollContainerEl,
+ axis: "block",
+});
+```
+
+```ts
+type ScrollAnimationOptions = Omit & {
+ source?: Element; // scroll container — defaults to document root scroller
+ axis?: "block" | "inline" | "x" | "y";
+};
+
+function makeScrollAnimation(
+ el: Element,
+ keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
+ options?: ScrollAnimationOptions,
+): Animation
+
+function createScrollAnimation(
+ target: Accessor,
+ keyframes: MaybeAccessor,
+ options?: MaybeAccessor,
+): Accessor
+```
+
+---
+
+## `makeViewAnimation` / `createViewAnimation`
+
+Plays a WAAPI animation whose progress is driven by an element's intersection with the scroll port via [`ViewTimeline`](https://developer.mozilla.org/en-US/docs/Web/API/ViewTimeline). Replaces the IntersectionObserver + class-toggle pattern.
+
+```ts
+// Animate the element itself as it enters the viewport
+const anim = createViewAnimation(
+ () => ref,
+ [{ opacity: 0, transform: "translateY(16px)" }, { opacity: 1, transform: "none" }],
+ { fill: "both" },
+);
+
+// Observe a different element than the one being animated
+const anim = createViewAnimation(() => animatedEl, keyframes, {
+ fill: "both",
+ subject: triggerEl,
+ inset: "0px 0px -100px 0px",
+});
+```
+
+```ts
+type ViewAnimationOptions = Omit & {
+ subject?: Element; // element to observe — defaults to target
+ axis?: "block" | "inline" | "x" | "y";
+ inset?: string | string[]; // shrinks/expands the intersection root
+};
+
+function makeViewAnimation(
+ el: Element,
+ keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
+ options?: ViewAnimationOptions,
+): Animation
+
+function createViewAnimation(
+ target: Accessor,
+ keyframes: MaybeAccessor,
+ options?: MaybeAccessor,
+): Accessor
+```
+
+---
+
+## `makeFlip`
+
+FLIP (First–Last–Invert–Play) layout animation. Call `snapshot()` before the DOM change to record the element's current geometry, then call `flip()` after to animate from the old position/size to the new one.
+
+```tsx
+let el!: HTMLUListElement;
+const { snapshot, flip } = makeFlip(el, { duration: 300, easing: "ease" });
+
+const handleReorder = () => {
+ snapshot();
+ setItems(prev => [...prev].reverse()); // DOM updates synchronously
+ flip();
+};
+
+return
...
;
+```
+
+`flip()` is a no-op if `snapshot()` was never called or if the geometry didn't change. It resets the captured rect after each call, so a second `flip()` without a new `snapshot()` is always a no-op.
+
+> **Note:** geometry is measured via `getBoundingClientRect` (viewport coordinates). Elements inside `position: fixed` or `position: absolute` ancestors may need coordinate adjustment.
+
+```ts
+function makeFlip(
+ el: Element,
+ options?: KeyframeAnimationOptions,
+): { snapshot: () => void; flip: () => Animation | undefined }
+```
+
+---
+
+## `makeStagger` / `createStagger`
+
+Applies a WAAPI animation to a list of elements with a per-element delay offset. The `stagger` option is added on top of the base `delay`.
+
+```ts
+// Imperative — animate a static list of elements
+makeStagger(listItems, [{ opacity: 0 }, { opacity: 1 }], {
+ duration: 400,
+ stagger: 60,
+});
+
+// Reactive — re-runs (cancelling previous animations) when the target list changes
+const itemRefs: HTMLLIElement[] = [];
+
+const anims = createStagger(
+ () => itemRefs,
+ [{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }],
+ { duration: 400, stagger: 60, easing: "ease-out" },
+);
+```
+
+```ts
+type StaggerOptions = KeyframeAnimationOptions & {
+ stagger?: number; // ms added per element on top of `delay`
+};
+
+function makeStagger(
+ els: Element[],
+ keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
+ options?: StaggerOptions,
+): Animation[]
+
+function createStagger(
+ targets: Accessor<(Element | null | undefined)[]>,
+ keyframes: MaybeAccessor,
+ options?: MaybeAccessor,
+): Accessor
+```
+
+---
+
+## `makeAnimationGroup` / `createAnimationGroup`
+
+Coordinates a list of `Animation` objects as a single unit. All five control methods are forwarded to every non-null animation simultaneously. Pairs naturally with `makeAnimate` and `makeStagger`.
+
+`makeAnimationGroup` takes a static array. `createAnimationGroup` takes an accessor and re-derives the group whenever the list changes — each control method always operates on the most recent set of animations.
+
+```ts
+// Imperative — static list
+const header = makeAnimate(headerEl, fadeIn, { duration: 300 });
+const body = makeAnimate(bodyEl, fadeIn, { duration: 300, delay: 100 });
+const footer = makeAnimate(footerEl, fadeIn, { duration: 300, delay: 200 });
+
+const group = makeAnimationGroup([header, body, footer]);
+
+group.pause();
+group.play();
+group.cancel();
+```
+
+```tsx
+// Reactive — list changes when items() changes
+const itemRefs: HTMLLIElement[] = [];
+const [items, setItems] = createSignal(data);
+
+const anims = createStagger(
+ () => itemRefs,
+ [{ opacity: 0 }, { opacity: 1 }],
+ { duration: 300, stagger: 40 },
+);
+
+// group.play() / pause() always targets the animations from the latest render
+const group = createAnimationGroup(anims);
+
+return (
+
+
+
+ {(item, i) =>
{item.name}
}
+
+
+);
+```
+
+```ts
+type AnimationGroupControls = {
+ play: () => void;
+ pause: () => void;
+ cancel: () => void;
+ reverse: () => void;
+ finish: () => void;
+};
+
+function makeAnimationGroup(
+ animations: (Animation | null | undefined)[],
+): AnimationGroupControls
+
+function createAnimationGroup(
+ animations: Accessor<(Animation | null | undefined)[]>,
+): AnimationGroupControls
+```
+
+---
+
+## `createPresenceAnimation`
+
+Manages mount/unmount lifecycle with WAAPI enter and exit animations. Pass a `target` ref accessor, a `show` signal, and enter/exit keyframes. The returned `isMounted` accessor should gate the element's presence in the DOM — the element stays mounted until its exit animation finishes.
+
+Exit keyframes default to the enter keyframes reversed. If `show` toggles back to `true` while an exit is in progress, the exit is cancelled and the enter restarts.
+
+```tsx
+const [show, setShow] = createSignal(false);
+let el!: HTMLDivElement;
+
+const { isMounted } = createPresenceAnimation(() => el, show, {
+ enter: [
+ { opacity: 0, transform: "translateY(8px)" },
+ { opacity: 1, transform: "none" },
+ ],
+ enterOptions: { duration: 250, easing: "ease-out" },
+ // exit defaults to reversed enter — fade out and slide down
+});
+
+return (
+ <>
+
+
+