From b23e871723a8ca6c8d2b040400f06d273cafd09d Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 13 Aug 2026 14:02:22 +0000 Subject: [PATCH 1/3] Make the Motion autoplay option opt-in The examples used data-option-no-autoplay more often than not, so the default was wrong: autoplay now defaults to false and playback on mount is enabled with the presence-based data-option-autoplay attribute. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8 --- packages/ui-motion/src/Motion.ts | 10 +++++----- packages/ui-motion/src/MotionScrollTimeline.ts | 6 +++--- packages/ui-motion/src/MotionSequence.ts | 11 ++++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/ui-motion/src/Motion.ts b/packages/ui-motion/src/Motion.ts index 02c56627..7b6624cf 100644 --- a/packages/ui-motion/src/Motion.ts +++ b/packages/ui-motion/src/Motion.ts @@ -29,8 +29,8 @@ export interface MotionProps extends BaseProps { * * Animate the component's root element declaratively with the * [Motion](https://motion.dev) library. The `initial` styles are applied on - * mount, then the `animate` keyframes play automatically unless `autoplay` is - * disabled with `data-option-no-autoplay`. + * mount, then the `animate` keyframes play when playback on mount is enabled + * with `data-option-autoplay`. * * The component holds a single current animation. `play()` and `reverse()` * always drive the animation declared by the options — recreating it when an @@ -55,7 +55,7 @@ export class Motion extends Base extends Base - *
+ *
* Content *
* diff --git a/packages/ui-motion/src/MotionSequence.ts b/packages/ui-motion/src/MotionSequence.ts index c3f2b9e9..1aa30903 100644 --- a/packages/ui-motion/src/MotionSequence.ts +++ b/packages/ui-motion/src/MotionSequence.ts @@ -25,17 +25,18 @@ export interface MotionSequenceProps extends BaseProps { * By default segments run one after another; a child's `at` option positions * its segment explicitly (a time in seconds, a relative offset like `"-0.2"`, * or `"<"` for "with the previous"), and the `stagger` option spreads the - * children automatically. Give the children `data-option-no-autoplay` — the - * sequence owns their playback. + * children automatically. The sequence owns the children's playback — leave + * their `autoplay` off (its default) and enable it on the sequence itself + * with `data-option-autoplay` to play on mount. * * Sequences need the full `motion` entry: `motion/mini`'s `animate()` does * not support them. * * @example * ```html - *
    - *
  • - *
  • + *
      + *
    • + *
    • *
    * ``` * From 68a1f45d8b56ba50cd3f1dc49a11aace313a857d Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 13 Aug 2026 14:02:35 +0000 Subject: [PATCH 2/3] Update the Motion tests for the opt-in autoplay Specs asserting playback on mount now set data-option-autoplay, specs asserting manual playback mount bare, and the default spec asserts that animate keyframes without the attribute do not play. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8 --- packages/tests/Motion/Motion.gestures.spec.ts | 2 +- packages/tests/Motion/Motion.spec.ts | 53 +++++++++++++------ .../tests/Motion/MotionScrollTimeline.spec.ts | 14 ++--- packages/tests/Motion/MotionSequence.spec.ts | 14 ++--- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/tests/Motion/Motion.gestures.spec.ts b/packages/tests/Motion/Motion.gestures.spec.ts index 4df089a0..9f228229 100644 --- a/packages/tests/Motion/Motion.gestures.spec.ts +++ b/packages/tests/Motion/Motion.gestures.spec.ts @@ -28,7 +28,7 @@ describe('Motion gesture options', () => { }); it('should bind nothing without gesture options', async () => { - await mountMotion({ dataOptionAnimate: '{ "x": 100 }', dataOptionNoAutoplay: '' }); + await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); expect(mockHover.fn).not.toHaveBeenCalled(); expect(mockPress.fn).not.toHaveBeenCalled(); diff --git a/packages/tests/Motion/Motion.spec.ts b/packages/tests/Motion/Motion.spec.ts index fe7af47e..8f33e324 100644 --- a/packages/tests/Motion/Motion.spec.ts +++ b/packages/tests/Motion/Motion.spec.ts @@ -50,17 +50,21 @@ describe('Motion component', () => { }); it('should apply the initial styles on mount without playing', async () => { - const { el, calls } = await mountMotion({ dataOptionInitial: '{ "opacity": 0 }' }); + const { el, calls } = await mountMotion({ + dataOptionInitial: '{ "opacity": 0 }', + dataOptionAutoplay: '', + }); expect(mockAnimate).toHaveBeenCalledTimes(1); expect(mockAnimate).toHaveBeenCalledWith(el, { opacity: 0 }, { duration: 0 }); expect(calls['motion-play']).toBe(0); }); - it('should autoplay the animate keyframes with the transition options', async () => { + it('should autoplay the animate keyframes with the transition options when enabled', async () => { const { el, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', dataOptionTransition: '{ "duration": 1 }', + dataOptionAutoplay: '', }); expect(mockAnimate).toHaveBeenCalledTimes(1); @@ -72,10 +76,9 @@ describe('Motion component', () => { expect(calls['motion-complete']).toBe(1); }); - it('should not autoplay when disabled, and play on demand', async () => { + it('should not autoplay by default, and play on demand', async () => { const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', - dataOptionNoAutoplay: '', }); expect(instance.$options.autoplay).toBe(false); @@ -92,7 +95,10 @@ describe('Motion component', () => { }); it('should replay the same animation on repeated play calls', async () => { - const { instance } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); instance.play(); await wait(0); @@ -104,7 +110,6 @@ describe('Motion component', () => { it('should reverse from the end when nothing has played yet', async () => { const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', - dataOptionNoAutoplay: '', }); instance.reverse(); @@ -118,7 +123,10 @@ describe('Motion component', () => { }); it('should flip the playback direction with reverse and play', async () => { - const { instance } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); instance.reverse(); await wait(0); @@ -132,7 +140,6 @@ describe('Motion component', () => { it('should pause the current animation, and ignore pause when idle', async () => { const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', - dataOptionNoAutoplay: '', }); instance.pause(); @@ -150,6 +157,7 @@ describe('Motion component', () => { const { el, instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', dataOptionTransition: '{ "duration": 1 }', + dataOptionAutoplay: '', }); instance.animate({ y: 50 }, { duration: 3 }); @@ -173,6 +181,7 @@ describe('Motion component', () => { const { el, instance } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', dataOptionTransition: '{ "duration": 1 }', + dataOptionAutoplay: '', }); instance.animate({ rotate: 360 }); @@ -199,7 +208,10 @@ describe('Motion component', () => { }); it('should stop the current animation and create a fresh one on the next play', async () => { - const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance, calls } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); instance.stop(); expect(animations[0].state).toBe('stopped'); @@ -212,7 +224,10 @@ describe('Motion component', () => { }); it('should cancel the current animation', async () => { - const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance, calls } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); instance.cancel(); expect(animations[0].state).toBe('cancelled'); @@ -221,7 +236,10 @@ describe('Motion component', () => { }); it('should jump to the end state with complete', async () => { - const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance, calls } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); instance.complete(); await wait(0); @@ -232,7 +250,6 @@ describe('Motion component', () => { it('should seek the current animation, creating it paused when idle', async () => { const { instance } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', - dataOptionNoAutoplay: '', }); await instance.seek(0.5); @@ -249,7 +266,6 @@ describe('Motion component', () => { it('should expose the playback state through getters', async () => { const { instance } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }', - dataOptionNoAutoplay: '', }); expect(instance.controls).toBeNull(); @@ -265,7 +281,11 @@ describe('Motion component', () => { }); it('should dispatch bubbling events', async () => { - const el = h('div', { dataComponent: 'Motion', dataOptionAnimate: '{ "x": 100 }' }); + const el = h('div', { + dataComponent: 'Motion', + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); const parent = h('div', [el]); let bubbled = 0; parent.addEventListener('motion-play', () => { @@ -280,7 +300,10 @@ describe('Motion component', () => { }); it('should stop the current animation on destroy without completing', async () => { - const { instance, calls } = await mountMotion({ dataOptionAnimate: '{ "x": 100 }' }); + const { instance, calls } = await mountMotion({ + dataOptionAnimate: '{ "x": 100 }', + dataOptionAutoplay: '', + }); await instance.$destroy(); expect(animations[0].state).toBe('stopped'); diff --git a/packages/tests/Motion/MotionScrollTimeline.spec.ts b/packages/tests/Motion/MotionScrollTimeline.spec.ts index 8564044e..ddebcf7c 100644 --- a/packages/tests/Motion/MotionScrollTimeline.spec.ts +++ b/packages/tests/Motion/MotionScrollTimeline.spec.ts @@ -1,16 +1,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Importing the mock injects the `motion` module double through `provideMotion` // before the components resolve it below. -import { animations, mockAnimate, scrollLinks, mockMotionModule, resetMockMotion } from './mock-motion.js'; +import { + animations, + mockAnimate, + scrollLinks, + mockMotionModule, + resetMockMotion, +} from './mock-motion.js'; import { MotionScrollTimeline } from '@studiometa/ui-motion'; import { h, wait } from '#test-utils'; function motionChild(animate: string) { - return h('div', { - dataComponent: 'Motion', - dataOptionAnimate: animate, - dataOptionNoAutoplay: '', - }); + return h('div', { dataComponent: 'Motion', dataOptionAnimate: animate }); } async function mountTimeline(attributes: Record = {}, children = 2) { diff --git a/packages/tests/Motion/MotionSequence.spec.ts b/packages/tests/Motion/MotionSequence.spec.ts index c8246c3d..ed0f3b1e 100644 --- a/packages/tests/Motion/MotionSequence.spec.ts +++ b/packages/tests/Motion/MotionSequence.spec.ts @@ -6,7 +6,7 @@ import { MotionSequence } from '@studiometa/ui-motion'; import { h, wait } from '#test-utils'; function motionChild(attributes: Record) { - return h('div', { dataComponent: 'Motion', dataOptionNoAutoplay: '', ...attributes }); + return h('div', { dataComponent: 'Motion', ...attributes }); } async function mountSequence( @@ -36,7 +36,7 @@ describe('MotionSequence component', () => { }); it('should autoplay one sequence built from the children in DOM order', async () => { - const { el, kids } = await mountSequence(); + const { el, kids } = await mountSequence({ dataOptionAutoplay: '' }); let played = 0; el.addEventListener('motion-play', () => (played += 1)); @@ -51,7 +51,7 @@ describe('MotionSequence component', () => { }); it('should position segments with the at option, parsing numbers', async () => { - const { kids } = await mountSequence({}, [ + const { kids } = await mountSequence({ dataOptionAutoplay: '' }, [ { dataOptionAnimate: '{ "x": 100 }', dataOptionAt: '0.5' }, { dataOptionAnimate: '{ "y": 50 }', dataOptionAt: '<' }, ]); @@ -63,7 +63,7 @@ describe('MotionSequence component', () => { }); it('should spread the segments with stagger, explicit at winning', async () => { - const { kids } = await mountSequence({ dataOptionStagger: '0.2' }, [ + const { kids } = await mountSequence({ dataOptionStagger: '0.2', dataOptionAutoplay: '' }, [ { dataOptionAnimate: '{ "x": 100 }' }, { dataOptionAnimate: '{ "y": 50 }' }, { dataOptionAnimate: '{ "rotate": 90 }', dataOptionAt: '2' }, @@ -77,17 +77,17 @@ describe('MotionSequence component', () => { }); it('should pass its transition as the sequence options', async () => { - await mountSequence({ dataOptionTransition: '{ "duration": 3 }' }); + await mountSequence({ dataOptionTransition: '{ "duration": 3 }', dataOptionAutoplay: '' }); expect(animations[0].options).toEqual({ duration: 3 }); }); it('should skip children without keyframes and not autoplay when none remain', async () => { - await mountSequence({}, [{}, {}]); + await mountSequence({ dataOptionAutoplay: '' }, [{}, {}]); expect(mockAnimate).not.toHaveBeenCalled(); }); it('should drive the whole sequence with the inherited playback surface', async () => { - const { instance } = await mountSequence({ dataOptionNoAutoplay: '' }); + const { instance } = await mountSequence(); expect(mockAnimate).not.toHaveBeenCalled(); instance.play(); From 19a5bfa827cdb9de522ff1c392794a20e7d5e266 Mon Sep 17 00:00:00 2001 From: Titouan Mathis Date: Thu, 13 Aug 2026 14:02:35 +0000 Subject: [PATCH 3/3] Update the Motion docs for the opt-in autoplay Rewrite the autoplay option docs (default false, enabled with data-option-autoplay), drop data-option-no-autoplay from every story and example, and add data-option-autoplay to the stories that play on mount. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FVXrJ8idMfvt667yJadvB8 --- packages/docs/reference/items/Motion/examples.md | 2 +- packages/docs/reference/items/Motion/index.md | 7 +++---- packages/docs/reference/items/Motion/js-api.md | 10 +++++----- .../reference/items/Motion/stories/action/app.twig | 1 - .../reference/items/Motion/stories/basic/app.twig | 1 + .../reference/items/Motion/stories/events/app.twig | 1 - .../items/Motion/stories/transport/app.twig | 1 - .../items/Motion/stories/with-dialog/app.twig | 1 - .../items/Motion/stories/with-timer/app.twig | 1 - .../reference/items/MotionScrollTimeline/index.md | 5 ++--- .../reference/items/MotionScrollTimeline/js-api.md | 5 ++--- .../stories/with-scroll-timeline/app.twig | 2 -- .../docs/reference/items/MotionSequence/index.md | 8 ++++---- .../docs/reference/items/MotionSequence/js-api.md | 8 ++++---- .../items/MotionSequence/stories/sequence/app.twig | 5 ----- packages/ui-motion/README.md | 14 +++++--------- 16 files changed, 27 insertions(+), 45 deletions(-) diff --git a/packages/docs/reference/items/Motion/examples.md b/packages/docs/reference/items/Motion/examples.md index d93e7549..2e7c7577 100644 --- a/packages/docs/reference/items/Motion/examples.md +++ b/packages/docs/reference/items/Motion/examples.md @@ -6,7 +6,7 @@ title: Motion examples ## Action-driven playback -Autoplay is disabled, so the card only moves when asked to. The buttons are [`Action`](/reference/items/Action/) components targeting the `Motion` instance with the arrow syntax, the hover pattern combines `play()` and `reverse()` for a symmetric in/out animation, and the last button runs a one-off spin with `animate()` — clicking Play afterwards returns to the animation declared by the options. +Autoplay is off by default, so the card only moves when asked to. The buttons are [`Action`](/reference/items/Action/) components targeting the `Motion` instance with the arrow syntax, the hover pattern combines `play()` and `reverse()` for a symmetric in/out animation, and the last button runs a one-off spin with `animate()` — clicking Play afterwards returns to the animation declared by the options. -The `Motion` component animates its root element declaratively with the [Motion](https://motion.dev) library. Describe the animation with the `initial`, `animate` and `transition` options: the `initial` styles are applied on mount, then the `animate` keyframes play automatically — or on demand when autoplay is disabled. +The `Motion` component animates its root element declaratively with the [Motion](https://motion.dev) library. Describe the animation with the `initial`, `animate` and `transition` options: the `initial` styles are applied on mount, then the `animate` keyframes play on demand — or automatically when autoplay is enabled with `data-option-autoplay`. The component is a thin, headless playback surface exposing imperative methods that an [`Action`](/reference/items/Action/) can call from any interaction: `play()` and `reverse()` always drive the animation declared by the options, while `animate()` runs a one-off to arbitrary keyframes. All its events bubble, so an ancestor `Action` can catch and route them; use the `.stop` event modifier to contain them in nested setups. @@ -35,14 +35,13 @@ Option values are parsed as JSON, so object keys must be quoted: ## Driving the animation with `Action` -Disable autoplay with `data-option-no-autoplay` and control the playback from any interaction — see the [examples](./examples.html) for a complete demo: +Autoplay is off by default, so the playback is yours to control from any interaction — see the [examples](./examples.html) for a complete demo: ```html
    + data-option-animate='{ "x": 100 }'> …
    diff --git a/packages/docs/reference/items/Motion/js-api.md b/packages/docs/reference/items/Motion/js-api.md index 902c6be3..a4fabe63 100644 --- a/packages/docs/reference/items/Motion/js-api.md +++ b/packages/docs/reference/items/Motion/js-api.md @@ -33,7 +33,7 @@ Styles applied to the element on mount, before anything plays. Use it to define - Type: `DOMKeyframesDefinition` - Default: `{}` -The target keyframes of the animation. They play automatically on mount unless [`autoplay`](#autoplay) is disabled. +The target keyframes of the animation. They play on mount when [`autoplay`](#autoplay) is enabled. ### `transition` @@ -56,16 +56,16 @@ Motion's animation options: `duration`, `delay`, `ease`, `type` (`"tween"`, `"sp ### `autoplay` - Type: `boolean` -- Default: `true` +- Default: `false` -Whether the `animate` keyframes play automatically on mount. Since the default is `true`, use the negated `data-option-no-autoplay` attribute to require an explicit `play()`. +Whether the `animate` keyframes play automatically on mount. Enable it with the `data-option-autoplay` attribute; without it, playback waits for an explicit `play()`. ```html {4}
    + data-option-autoplay> …
    ``` @@ -183,7 +183,7 @@ Methods are callable from an `Action` effect — on the same element or through ```html {2,3}
    - +
    ``` diff --git a/packages/docs/reference/items/Motion/stories/action/app.twig b/packages/docs/reference/items/Motion/stories/action/app.twig index cdb027d5..9a8abfe6 100644 --- a/packages/docs/reference/items/Motion/stories/action/app.twig +++ b/packages/docs/reference/items/Motion/stories/action/app.twig @@ -3,7 +3,6 @@ data-component="Motion" data-option-animate='{ "x": 160, "rotate": 10 }' data-option-transition='{ "type": "spring", "bounce": 0.4 }' - data-option-no-autoplay class="px-6 py-4 rounded-lg bg-blue-400 dark:bg-blue-600 text-white font-bold"> Drive me!
diff --git a/packages/docs/reference/items/Motion/stories/basic/app.twig b/packages/docs/reference/items/Motion/stories/basic/app.twig index 8776ef62..42dd958a 100644 --- a/packages/docs/reference/items/Motion/stories/basic/app.twig +++ b/packages/docs/reference/items/Motion/stories/basic/app.twig @@ -3,6 +3,7 @@ data-option-initial='{ "opacity": 0, "y": 24, "scale": 0.9 }' data-option-animate='{ "opacity": 1, "y": 0, "scale": 1 }' data-option-transition='{ "type": "spring", "bounce": 0.3, "duration": 0.8 }' + data-option-autoplay class="px-6 py-4 rounded-lg bg-blue-400 dark:bg-blue-600 text-white font-bold"> Hello, I animate on mount! diff --git a/packages/docs/reference/items/Motion/stories/events/app.twig b/packages/docs/reference/items/Motion/stories/events/app.twig index 3112fc83..107d9999 100644 --- a/packages/docs/reference/items/Motion/stories/events/app.twig +++ b/packages/docs/reference/items/Motion/stories/events/app.twig @@ -10,7 +10,6 @@ data-component="Motion" data-option-animate='{ "x": 160 }' data-option-transition='{ "duration": 1.5 }' - data-option-no-autoplay class="px-6 py-4 rounded-lg bg-blue-400 dark:bg-blue-600 text-white font-bold"> Watch my status diff --git a/packages/docs/reference/items/Motion/stories/transport/app.twig b/packages/docs/reference/items/Motion/stories/transport/app.twig index 5e5c3e82..ccd764c0 100644 --- a/packages/docs/reference/items/Motion/stories/transport/app.twig +++ b/packages/docs/reference/items/Motion/stories/transport/app.twig @@ -3,7 +3,6 @@ data-component="Motion" data-option-animate='{ "x": 200, "rotate": 180 }' data-option-transition='{ "duration": 2, "ease": "easeInOut" }' - data-option-no-autoplay class="size-16 rounded-lg bg-blue-400 dark:bg-blue-600">
diff --git a/packages/docs/reference/items/Motion/stories/with-dialog/app.twig b/packages/docs/reference/items/Motion/stories/with-dialog/app.twig index 6d4b30ab..0155eeb7 100644 --- a/packages/docs/reference/items/Motion/stories/with-dialog/app.twig +++ b/packages/docs/reference/items/Motion/stories/with-dialog/app.twig @@ -31,7 +31,6 @@ data-option-initial='{ "opacity": 0, "scale": 0.8, "y": 40 }' data-option-animate='{ "opacity": 1, "scale": 1, "y": 0 }' data-option-transition='{ "type": "spring", "bounce": 0.4 }' - data-option-no-autoplay class="pointer-events-auto relative w-full max-w-lg p-8 rounded-lg bg-white text-black shadow-2xl">

Spring entrance and exit

diff --git a/packages/docs/reference/items/Motion/stories/with-timer/app.twig b/packages/docs/reference/items/Motion/stories/with-timer/app.twig index 260bcea9..6d625383 100644 --- a/packages/docs/reference/items/Motion/stories/with-timer/app.twig +++ b/packages/docs/reference/items/Motion/stories/with-timer/app.twig @@ -7,7 +7,6 @@ data-component="Action Motion TimerProgress" data-option-delay="8" data-option-repeat - data-option-no-autoplay data-option-animate='{ "x": 200, "rotate": 360, "borderRadius": "50%" }' data-on:timer-progress="Motion.seek(1 - Math.abs(2 * event.detail[0] - 1))" class="size-16 rounded-lg bg-blue-400 dark:bg-blue-600">

diff --git a/packages/docs/reference/items/MotionScrollTimeline/index.md b/packages/docs/reference/items/MotionScrollTimeline/index.md index f4e3d4a0..533c7cb8 100644 --- a/packages/docs/reference/items/MotionScrollTimeline/index.md +++ b/packages/docs/reference/items/MotionScrollTimeline/index.md @@ -6,7 +6,7 @@ badges: [JS] The `MotionScrollTimeline` component is the scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every [`Motion`](/reference/items/Motion/) child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. -The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; add `data-option-no-autoplay` so they do not play before the scroll link takes over. Registering the timeline is enough: it mounts its `Motion` children itself. +The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; leave their `autoplay` off (its default) so they do not play before the scroll link takes over. Registering the timeline is enough: it mounts its `Motion` children itself. `MotionScrollTimeline` is part of `@studiometa/ui-motion`, alongside [`Motion`](/reference/items/Motion/), [`MotionSequence`](/reference/items/MotionSequence/) and [`MotionView`](/reference/items/MotionView/). @@ -19,8 +19,7 @@ The children declare their keyframes as usual (arrays give multi-step tracks) an class="h-[300vh]">
+ data-option-animate='{ "opacity": [0, 1, 0], "y": [80, 0, -80] }'> …
diff --git a/packages/docs/reference/items/MotionScrollTimeline/js-api.md b/packages/docs/reference/items/MotionScrollTimeline/js-api.md index 56efb662..eee38cd2 100644 --- a/packages/docs/reference/items/MotionScrollTimeline/js-api.md +++ b/packages/docs/reference/items/MotionScrollTimeline/js-api.md @@ -4,7 +4,7 @@ title: MotionScrollTimeline JS API # JS API -The scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every [`Motion`](/reference/items/Motion/) child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; add `data-option-no-autoplay` so they do not play before the scroll link takes over. +The scroll driver for a group of animations: the element's traversal of the viewport defines the timeline, and every [`Motion`](/reference/items/Motion/) child it contains is bound to that progress with Motion's [`scroll()`](https://motion.dev/docs/scroll) — hardware-accelerated where the browser supports `ScrollTimeline`. The children declare their keyframes as usual (arrays give multi-step tracks) and keep their whole playback surface; leave their `autoplay` off (its default) so they do not play before the scroll link takes over. ```html {2,6} @@ -13,8 +13,7 @@ The scroll driver for a group of animations: the element's traversal of the view class="h-[300vh]">
+ data-option-animate='{ "opacity": [0, 1, 0], "y": [80, 0, -80] }'> …
diff --git a/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig b/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig index 396030d9..96a93650 100644 --- a/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig +++ b/packages/docs/reference/items/MotionScrollTimeline/stories/with-scroll-timeline/app.twig @@ -8,14 +8,12 @@

Driven by scroll

diff --git a/packages/docs/reference/items/MotionSequence/index.md b/packages/docs/reference/items/MotionSequence/index.md index 605cf8e5..a6a4e390 100644 --- a/packages/docs/reference/items/MotionSequence/index.md +++ b/packages/docs/reference/items/MotionSequence/index.md @@ -6,7 +6,7 @@ badges: [JS] The `MotionSequence` component orchestrates its [`Motion`](/reference/items/Motion/) children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). -The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. Give the children `data-option-no-autoplay` — the sequence owns their playback. +The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. The sequence owns the children's playback — leave their `autoplay` off (its default) and enable it on the sequence itself with `data-option-autoplay` to play on mount. `MotionSequence` is part of `@studiometa/ui-motion`, alongside [`Motion`](/reference/items/Motion/), [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) and [`MotionView`](/reference/items/MotionView/). @@ -14,9 +14,9 @@ The whole playback surface applies to the sequence: an [`Action`](/reference/ite ```html {1} -
    -
  • One
  • -
  • Two
  • +
      +
    • One
    • +
    • Two
    ``` diff --git a/packages/docs/reference/items/MotionSequence/js-api.md b/packages/docs/reference/items/MotionSequence/js-api.md index e6a956b5..d41553a6 100644 --- a/packages/docs/reference/items/MotionSequence/js-api.md +++ b/packages/docs/reference/items/MotionSequence/js-api.md @@ -4,13 +4,13 @@ title: MotionSequence JS API # JS API -Orchestrate the [`Motion`](/reference/items/Motion/) children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. Give the children `data-option-no-autoplay` — the sequence owns their playback. +Orchestrate the [`Motion`](/reference/items/Motion/) children as one animation sequence: each child declares its keyframes as usual, and the sequence composes them — in DOM order — into a single timeline with Motion's [sequencing](https://motion.dev/docs/animate#timeline-sequencing). The whole playback surface applies to the sequence: an [`Action`](/reference/items/Action/) can `play()`, `reverse()` or `seek()` the entire choreography, and a [`MotionScrollTimeline`](/reference/items/MotionScrollTimeline/) can scrub it. The sequence owns the children's playback — leave their `autoplay` off (its default) and enable it on the sequence itself with `data-option-autoplay` to play on mount. ```html {1} -
      -
    • One
    • -
    • Two
    • +
        +
      • One
      • +
      • Two
      ``` diff --git a/packages/docs/reference/items/MotionSequence/stories/sequence/app.twig b/packages/docs/reference/items/MotionSequence/stories/sequence/app.twig index b7554ad7..9562a84e 100644 --- a/packages/docs/reference/items/MotionSequence/stories/sequence/app.twig +++ b/packages/docs/reference/items/MotionSequence/stories/sequence/app.twig @@ -2,13 +2,11 @@
      • One
      • @@ -16,7 +14,6 @@ data-component="Motion" data-option-initial='{ "opacity": 0, "x": -32 }' data-option-animate='{ "opacity": 1, "x": 0 }' - data-option-no-autoplay class="px-4 py-2 rounded bg-blue-400 dark:bg-blue-600 text-white font-bold"> Two @@ -24,7 +21,6 @@ data-component="Motion" data-option-initial='{ "opacity": 0, "x": -32 }' data-option-animate='{ "opacity": 1, "x": 0 }' - data-option-no-autoplay class="px-4 py-2 rounded bg-blue-400 dark:bg-blue-600 text-white font-bold"> Three @@ -34,7 +30,6 @@ data-option-animate='{ "opacity": 1, "scale": 1 }' data-option-transition='{ "type": "spring", "bounce": 0.5 }' data-option-at="+0.2" - data-option-no-autoplay class="px-4 py-2 rounded bg-emerald-400 dark:bg-emerald-600 text-white font-bold"> Finale, 0.2s later diff --git a/packages/ui-motion/README.md b/packages/ui-motion/README.md index b950e173..7b0be10e 100644 --- a/packages/ui-motion/README.md +++ b/packages/ui-motion/README.md @@ -29,15 +29,16 @@ registerComponent(Motion); data-component="Motion" data-option-initial='{ "opacity": 0, "y": 24 }' data-option-animate='{ "opacity": 1, "y": 0 }' - data-option-transition='{ "type": "spring", "bounce": 0.3 }'> + data-option-transition='{ "type": "spring", "bounce": 0.3 }' + data-option-autoplay> Hello world! ``` -The `initial` styles are applied on mount, then the `animate` keyframes play automatically. Disable the automatic playback with `data-option-no-autoplay` and drive the animation from an [`Action`](https://ui.studiometa.dev/reference/items/Action/) instead: +The `initial` styles are applied on mount, then the `animate` keyframes play when playback on mount is enabled with `data-option-autoplay`. Without it, drive the animation from an [`Action`](https://ui.studiometa.dev/reference/items/Action/) instead: ```html -
        ...
        +
        ...
        @@ -58,12 +59,7 @@ The `hover`, `press` and `inView` options apply keyframes while their state hold ```html
        -
        - ... -
        +
        ...
        ```