ScrollTimeline Engine¶
This page is a practical guide to using the ScrollTimeline engine. Read Engines Overview when you want side-by-side comparisons and tradeoffs.
The ScrollTimeline Engine is a lightweight engine that uses the Browsers native ScrollTimeline API.
It ties animation progress to the scroll position of a scrollable element. As the user scrolls, the
animation progresses — no AnimState required. update and subscriptions are optional, and only
needed if you want to react to lifecycle events.
Example¶
Scroll the page, and the progress bar will animate in response.
View Example
View Source Code
port module Animation.ScrollTimeline.Main exposing (main)
import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.ScrollTimeline as ScrollTimeline exposing (Container(..))
import Anim.Extra.Color as Color exposing (Color)
import Anim.Property.CustomColor as CustomColor exposing (ColorProperty(..))
import Anim.Property.Scale as Scale
import Browser
import Html exposing (Html, div, h2, p, span, text)
import Html.Attributes exposing (class, id, style)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
-- PORTS
port motionCmd : Encode.Value -> Cmd msg
-- MAIN
main : Program () () msg
main =
Browser.element
{ init = \_ -> init
, view = view
, update = \_ model -> ( model, Cmd.none )
, subscriptions = always Sub.none
}
-- ANIMATION
progressBarAnim : String
progressBarAnim =
"scrollProgress"
scrollProgress : ScrollTimeline.EngineBuilder -> ScrollTimeline.EngineBuilder
scrollProgress =
ScrollTimeline.for progressBarAnim
>> Scale.begin
>> Scale.fromX 0
>> Scale.toX 1
>> Scale.end
>> CustomColor.begin BackgroundColor
>> CustomColor.from Color.red
>> CustomColor.to Color.green
>> CustomColor.end
-- INIT
init : ( (), Cmd msg )
init =
( ()
, ScrollTimeline.animate motionCmd Document scrollProgress
)
-- VIEW
view : () -> Html msg
view _ =
div
[ style "font-family" "system-ui, sans-serif"
, style "color" "#1f2937"
, style "background" "#ffffff"
]
[ -- Fixed progress bar at top of page
div
[ style "position" "fixed"
, style "top" "0"
, style "left" "0"
, style "width" "100%"
, style "height" "5px"
, style "background" "#e5e7eb"
, style "z-index" "100"
]
[ div
(ScrollTimeline.attributes progressBarAnim
++ [ style "width" "100%"
, style "height" "100%"
, style "transform-origin" "left center"
, style "transform" "scaleX(0)"
]
)
[]
]
, div
[ style "text-align" "center"
, style "padding" "clamp(48px, 10vh, 80px) clamp(20px, 5vw, 40px) clamp(28px, 6vh, 40px)"
, style "background" "linear-gradient(135deg, #ede9fe, #ddd6fe)"
]
[ h2
[ style "font-size" "clamp(1.6rem, 4vw + 0.8rem, 2.5rem)"
, style "font-weight" "700"
, style "margin" "0 0 16px"
, style "color" "#4c1d95"
]
[ text "Scroll Timeline" ]
, p
[ style "font-size" "clamp(0.95rem, 1vw + 0.6rem, 1.1rem)"
, style "color" "#6d28d9"
, style "margin" "0"
]
[ text "Scroll down and watch the bar fill as the page moves from top to bottom." ]
]
-- Scrollable content cards
, div
[ style "max-width" "700px"
, style "margin" "0 auto"
, style "padding" "clamp(36px, 8vh, 60px) clamp(16px, 5vw, 40px)"
, style "display" "flex"
, style "flex-direction" "column"
, style "gap" "clamp(28px, 6vh, 60px)"
]
(List.map contentCard cards)
]
type alias CardData =
{ label : String
, color : String
, title : String
, body : String
}
cards : List CardData
cards =
[ { label = "01"
, color = "#6366f1"
, title = "Top to bottom"
, body = "The timeline starts at 0% at the top of the page and reaches 100% at the bottom."
}
, { label = "02"
, color = "#8b5cf6"
, title = "One scroll, two effects"
, body = "The same timeline drives both size and color, so one scroll gesture controls the whole bar."
}
, { label = "03"
, color = "#a78bfa"
, title = "Read progress at a glance"
, body = "Short red bar means early in the page, long green bar means you are near the end."
}
, { label = "04"
, color = "#7c3aed"
, title = "Simple trigger"
, body = "Call ScrollTimeline.animate once in init, then the browser keeps everything in sync while you scroll."
}
, { label = "05"
, color = "#5b21b6"
, title = "Easy to reuse"
, body = "Attach ScrollTimeline.attributes to any element and map scroll progress to the properties you want."
}
]
contentCard : CardData -> Html msg
contentCard card =
div
[ style "display" "flex"
, style "gap" "clamp(14px, 3vw, 24px)"
, style "align-items" "flex-start"
, style "padding" "clamp(20px, 4vw, 32px)"
, style "background" "white"
, style "border-radius" "16px"
, style "box-shadow" "0 4px 24px rgba(99,102,241,0.08)"
]
[ span
[ style "font-size" "clamp(1.4rem, 3vw + 0.4rem, 2rem)"
, style "font-weight" "800"
, style "color" card.color
, style "flex-shrink" "0"
, style "line-height" "1"
, style "padding-top" "4px"
]
[ text card.label ]
, div []
[ h2
[ style "font-size" "clamp(1.05rem, 1.5vw + 0.5rem, 1.3rem)"
, style "font-weight" "700"
, style "margin" "0 0 10px"
, style "color" "#111827"
]
[ text card.title ]
, p
[ style "font-size" "clamp(0.9rem, 1vw + 0.5rem, 1rem)"
, style "line-height" "1.7"
, style "color" "#6b7280"
, style "margin" "0"
]
[ text card.body ]
]
]
Quick Walkthrough¶
Here's a general workflow to get up an running quickly.
1. Build¶
View Source Code
2. Render¶
Render attributes on the element being animated.
View Source Code
3. Trigger with animate¶
Call animate to send a fire-and-forget scroll-driven animation command.
View Source Code
port module Main exposing (main)
import Anim.Engine.ScrollTimeline as ScrollTimeline
import Json.Encode
port motionCmd : Json.Encode.Value -> Cmd msg
startScrollAnimation : Cmd Msg
startScrollAnimation =
ScrollTimeline.animate motionCmd ScrollTimeline.Document <|
ScrollTimeline.for "progress"
>> scrollAnimation
4. Optional React¶
Subscribe only when you need lifecycle events in Elm. See Subscriptions and Update for full event handling.
View Source Code
import Json.Decode
port motionMsg : (Json.Decode.Value -> msg) -> Sub msg
type Msg
= GotScrollMsg ScrollTimeline.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotScrollMsg animMsg ->
case ScrollTimeline.update animMsg of
Just (ScrollTimeline.Ended _) ->
( model, Cmd.none )
_ ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions _ =
ScrollTimeline.subscriptions GotScrollMsg motionMsg
In Detail¶
Trigger¶
This engine uses the same JavaScript companion as the WAAPI engine, but only the outgoing port is needed, the incoming port is optional.
📖 See WAAPI JavaScript for CDN and NPM install instructions.
Browser support
ScrollTimeline is part of the CSS Scroll-Driven Animations spec. Check caniuse.com for current browser support.
The @phollyer/elm-motion companion automatically loads the scroll-timeline-polyfill when the native API is not available.
Fire-and-forget. Returns a Cmd msg with no state to store.
View Source Code
📖 See Triggering Animations for more info.
Update¶
Use update to process incoming messages and return a Maybe AnimEvent.
View Source Code
Events¶
The ScrollTimeline, ViewTimeline and WAAPI Engines all utilize the JavaScript Web Animations API, and they all use the same ports to communicate with the JS companion. If you use two or more of these engines in your Elm App, depending on your setup, there is the potential for them all to receive the same messages from JS at the same time, which could be confusing.
The library has you covered here though, all incoming messages are gated by each Engine, which is why update returns a Maybe AnimEvent - Nothing means the message was not for this Engine.
Every event carries the animation group name. Some events carry an additional value:
Iterationincludes the iteration count (Int)AnimErrorcarries an error string from the JavaScript layer
View Source Code
handleAnimEvent : Maybe ScrollTimeline.AnimEvent -> Model -> ( Model, Cmd Msg )
handleAnimEvent maybeEvent model =
case maybeEvent of
Just (ScrollTimeline.Ended "hero-card") ->
( model, Cmd.none )
Just (ScrollTimeline.Iteration "hero-card" count) ->
( model, Cmd.none )
Just (ScrollTimeline.AnimError err) ->
( model, Cmd.none )
_ ->
( model, Cmd.none )
| Event | Fires when... |
|---|---|
Started |
Animation begins playing |
Ended |
Animation completes |
Cancelled |
Animation is cancelled before completing |
Iteration |
Each iteration completes (looping or alternating) |
AnimError |
The JavaScript layer reports an error |
Subscriptions¶
Pass the message constructor and the incoming events port to receive lifecycle events.
View Source Code
📖 See React for more info.
View¶
Apply attributes to the animated element to attach the required animation group identifier.
View Source Code
📖 See Render for more info.
Axis¶
Vertical scroll is the default. Use horizontal in the animation pipeline when the container scrolls left and right.
View Source Code
Playback¶
iterations and alternate work the same as in other engines, but loopForever is not supported - it makes no sense for a scroll driven timeline.
📖 See Playback for iterations and alternate APIs with live examples.
Easing¶
Set the default easing for all properties that don't override it:
View Source Code
📖 See Easing for available easing functions.
Spring¶
Set the default spring for all properties that don't override it: The spring's motion is pre-baked into densely-spaced keyframe stops driven by the scroll timeline.
View Source Code
📖 See Spring for the full preset list and tuning guidance.
Discrete Properties¶
The ScrollTimeline engine manages discrete properties as inline styles. discreteEntry values are applied immediately when the animation starts, and discreteExit values flip when the animation completes. No additional view setup is needed.
📖 See Discrete Properties for the full API, live examples, and source code.
Transform Order¶
Use transformOrder to set the order in which transform properties are applied.
Call it after for to set the current animation group's order.
Call it before selecting a group to set the global default for groups that do not override it.
View Source Code
📖 See Transform Order for full details.
When to Choose This Engine¶
Choose ScrollTimeline when animation progress should be directly tied to scroll position.
- Best for: progress bars, scroll-driven reveals, and container-linked choreography.
- Avoid when: you need a time based Engine with related behaviour.
API Quick Reference¶
Types¶
| Type | Description |
|---|---|
AnimBuilder eng |
Carries all animation configuration |
AnimMsg |
Internal engine messages |
AnimEvent |
Events returned by update |
AnimGroupName |
String type alias for the animation group name |
Container |
Scroll source — Document or Container "id" |
TransformProperty |
Custom transform ordering |
Trigger¶
| Function | Type | Description |
|---|---|---|
animate |
(Value -> Cmd msg) -> Container -> (AnimBuilder eng -> AnimBuilder eng) -> Cmd msg |
Fire-and-forget scroll-driven animation |
Events¶
| Event | Description |
|---|---|
Ended AnimGroupName |
Animation completes |
Cancelled AnimGroupName Float |
Animation cancelled; Float is progress at cancellation |
Iteration AnimGroupName Int |
Loop iteration completes; Int is iteration count |
AnimError String |
JavaScript-layer error |
Update¶
| Function | Type | Description |
|---|---|---|
update |
AnimMsg -> Maybe AnimEvent |
Process messages and return an optional event |
Subscriptions¶
| Function | Type | Description |
|---|---|---|
subscriptions |
(AnimMsg -> msg) -> ((Value -> msg) -> Sub msg) -> Sub msg |
Subscribe to animation events from JavaScript |
View¶
| Function | Type | Description |
|---|---|---|
attributes |
AnimGroupName -> List (Html.Attribute msg) |
Attach the animation group identifier to an element |
Axis¶
| Function | Type | Description |
|---|---|---|
horizontal |
AnimBuilder eng -> AnimBuilder eng |
Use horizontal scroll as the timeline source |
Playback¶
| Function | Type | Description |
|---|---|---|
iterations |
Int -> AnimBuilder eng -> AnimBuilder eng |
Set number of iterations |
alternate |
AnimBuilder eng -> AnimBuilder eng |
Reverse direction on each iteration |
Easing¶
| Function | Type | Description |
|---|---|---|
easing |
Easing -> AnimBuilder eng -> AnimBuilder eng |
Set the easing function |
Spring¶
| Function | Type | Description |
|---|---|---|
spring |
Spring -> AnimBuilder eng -> AnimBuilder eng |
Set spring physics |
Discrete Properties¶
| Function | Type | Description |
|---|---|---|
discreteEntry |
String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a CSS property value when the animation starts |
discreteExit |
String -> String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a CSS property value during and after the animation |
Transform Order¶
| Function | Type | Description |
|---|---|---|
transformOrder |
List TransformProperty -> AnimBuilder eng -> AnimBuilder eng |
Set custom transform order |
For complete API details, see the Anim.Engine.ScrollTimeline documentation.
Next Steps¶
Explore the ViewTimeline Engine:
Or review migration paths and tradeoffs.