Skip to content

Scroll driven lottie animations - #2473

Open
tf wants to merge 14 commits into
codevise:masterfrom
tf:lottie-scroll
Open

Scroll driven lottie animations#2473
tf wants to merge 14 commits into
codevise:masterfrom
tf:lottie-scroll

Conversation

@tf

@tf tf commented Aug 18, 2026

Copy link
Copy Markdown
Member

REDMINE-21349

tf added 14 commits August 18, 2026 14:34
Content elements can now observe how far they have travelled through
the viewport, not only whether they are visible. Registering a content
element type with `viewTimeline: true` makes the
`useContentElementViewTimelineProgress` hook available, which invokes a
callback with a progress value between 0 and 1.

Vocabulary follows CSS scroll driven animations: The content element
acts as the subject of a view timeline and the `range` option selects
which part of it to measure (`cover`, `contain`, `entry` or `exit`).

Progress is passed to a callback instead of being returned by the hook
to prevent rerendering content elements on every scroll frame. It is
meant to drive imperative APIs like animation players.

Since content element types can support view timelines without always
observing scroll position, scroll and resize handlers are only
registered while there are subscriptions.

Specs can drive progress via the new `simulateScrollProgress` function
returned by `renderInContentElement`.
The new `scroll` playback mode turns the animation into a scrubbing
target: Instead of playing on its own, the current frame follows the
element's progress along the `cover` range of its view timeline. The
animation starts as soon as the element enters the viewport and
reaches its last frame once the element has completely left it.

Frames can only be set once the animation has loaded. Progress
observed before that is applied as soon as the load event fires.
Lets editors pick the new playback mode that couples the animation to
the scroll position of the element.
Content elements with standAlone position are pinned in the center of
the viewport while their scroll space passes by. Measuring their own
bounding rect would make progress stall for exactly the part of the
scroll space that the extra scrolling was added for.

Let components that add scroll space around a content element pass the
element that keeps moving with the page as the subject of the view
timeline.
Content elements with sticky position stay pinned next to the text
while the rest of their group scrolls past. Measuring their own
bounding rect would make progress stall for exactly that part of the
section.

The group is the containing block that constrains the sticky box, so
its bounding rect covers the same range of the page: from the element
scrolling in at the top of the group to it leaving with the group's
bottom edge.

On narrow viewports, sticky boxes are rendered inline. Progress is
then measured along the element itself again.

Extract a Box component for the box element on the way, since looking
up the group requires a ref.
Content elements that are pinned in the viewport are measured along a
taller subject: the scroll space for standAlone position, the group for
sticky position. Ranges other than cover took the subject's height for
the element's height, so contain meant "while the group is completely
inside the viewport" instead of "while the element is" - a range that
collapses into a jump from 0 to 1 as soon as the group is taller than
the viewport.

Express ranges as pairs of milestones along the page instead, and let
pinning components pass the pinned element next to the subject via a
provider component, so the element's height can be taken into account.

Elements that overflow their subject never reach their pinned position
and keep moving with the page. Fall back to measuring their own rect,
which then covers the same range of the page.
Content elements count as active while they intersect the vertical
center of the viewport. Autoplayed videos start playing at that point.
Add a range covering the same part of the page, so scroll coupled
animations can run exactly while the element holds the center of
attention.

Also correct the description of isActive in the docs, which claimed
elements are active while completely inside the viewport.
Content elements with sticky or standAlone position are pinned in the
viewport for part of their scroll space. Add a range measuring exactly
that part of the page, so scroll coupled animations can run while the
element holds its featured spot.

The position an element is pinned at is only known while it actually is
pinned. Measure how far the element has slid within its subject instead:
That distance grows from zero to the subject's extra scroll space while
the element is pinned and stays constant before and after, which clamps
progress on both ends.

Elements that are not pinned at all have no such phase. Progress along
the range stays 1 for them.
Which part of the page a content element holds the reader's attention
for depends on its position: Elements that are pinned in the viewport do
so while they stay in place, all others while they pass the center of
the viewport.

Add a range that resolves to the pinned or the center range accordingly,
so content elements do not have to know whether the layout pins them -
which can change with the viewport width.
Scroll coupled animations so far always ran along the whole part of the
page during which the element was visible. Add a select that lets
editors pick which part of the element's scroll motion drives the
animation, including the phase during which sticky and standAlone
elements stay in place.

The inFocus range means different things depending on position, so
name it after the pinned phase for positions that keep the element in
place and after the viewport center for all others. Since the texts of
a select cannot depend on other attributes, there are two inputs for
the same property toggled via visible bindings. Which of them applies
follows the resolved position, since layouts that do not support sticky
position render such elements inline.

Extend simulateScrollProgress with a range option, so specs can tell
which range a content element observes. Let the SelectInput domino read
the texts a select offers and the input dominos filter for inputs that
are currently visible.
Some input views render miniatures of sections or content elements to
illustrate what an option does. Those components are not input views
themselves and are shared between input views, so keep them in a
directory of their own.

SectionPaddingVisualizationView stays where it is despite its name: It
is an input view.
The illustration of how a content element behaves while the page scrolls
is useful beyond choosing a position: Scroll ranges can be demonstrated
with the same miniature section.

Extract a ContentElementVisualization component that renders the markup
and takes position and layout as props, and a useScrollAnimation hook
that scrolls a referenced element back and forth. Children of the
visualization are rendered inside the rectangle representing the content
element, so callers can add overlays.

Name it after the element it visualizes rather than after the section it
renders, to keep it apart from SectionVisualization, which illustrates
layout and appearance of sections.

Let callers compute the scroll position from the rendered visualization
instead of passing a fixed distance, since demonstrating a whole view
timeline requires knowing where the element sits.
Editors cannot tell from range names alone which part of the page drives
the animation. Illustrate each range with the same miniature section the
position select uses, scrolled along the whole view timeline of the
element: from before it enters the viewport until it has left again.

Two tweaks to the illustration make ranges easier to tell apart: The
rectangle representing the element is narrowed for positions that do not
place it next to text, and the section gets room to scroll above and
below its content. Without that room, the scroll position the end of the
timeline requires is beyond what the visualization can scroll to, which
makes progress appear to stop short of the end. Ranges that are measured
against the center of the viewport mark it with a dotted line.

Play progress is displayed inside the rectangle representing the content
element, as a percentage over a bar that fills the rectangle. Progress
comes from the same getViewTimelineProgress function the frontend uses,
so the demo cannot drift from the real behavior. Since the visualization
pins elements like the entry does, feeding it the rects measured inside
the visualization is enough.

All options illustrate the position the element currently has, since the
range is what differs between them. Position and layout can be passed as
functions, since options are only rendered once the dropdown opens.
Replace the plain selects for the scroll range with the new scroll
range select input view, so editors see the range they pick played back
on a miniature of their section instead of having to imagine it.

Both wordings of the inFocus range keep their own input, and both
illustrate the element as it appears in its section: The position is
resolved through the layout of the section, and the layout decides
whether text sits next to the element.

Options only exist while the dropdown is open, so specs read the range
names from the descriptions of the open dropdown. Export the input
domino from pageflow so looking up an input that is not a select does
not have to go through SelectInput.
@tf tf added this to the v17.2 milestone Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant