Skip to content

ViewTimeline Engine

This page is a practical guide to using the ViewTimeline engine. Read Engines Overview when you want side-by-side comparisons and tradeoffs.

The ViewTimeline Engine is a lightweight engine that uses the Browsers native ViewTimeline API. It ties animation progress to the view position of an element inside a scrollable container. As the user scrolls the element into, then out of, view, 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 different sections will fade in and slide up as they are scrolled into view.

View Example

View Source Code
port module Animation.ViewTimeline.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.ViewTimeline as ViewTimeline exposing (Range(..), Unit(..))
import Anim.Property.Opacity as Opacity
import Anim.Property.Translate as Translate
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


revealCard : String -> ViewTimeline.EngineBuilder -> ViewTimeline.EngineBuilder
revealCard animGroupName =
    ViewTimeline.for animGroupName
        >> Opacity.begin
        >> Opacity.from 0
        >> Opacity.to 1
        >> Opacity.end
        >> Translate.begin
        >> Translate.fromY 100
        >> Translate.toY 0
        >> Translate.end



-- INIT


init : ( (), Cmd msg )
init =
    ( ()
    , cards
        |> List.map
            (\card ->
                ViewTimeline.animate motionCmd <|
                    ViewTimeline.rangeStart (Entry 10 Perc)
                        >> ViewTimeline.rangeEnd (Cover 30 Perc)
                        >> ViewTimeline.easing BounceInOut
                        >> revealCard card.animGroupName
            )
        |> Cmd.batch
    )



-- VIEW


view : () -> Html msg
view _ =
    div
        [ style "font-family" "system-ui, sans-serif"
        , style "color" "#1f2937"
        , style "background" "#f9fafb"
        ]
        [ -- Page header
          div
            [ style "text-align" "center"
            , style "padding" "80px 40px 60px"
            , style "background" "linear-gradient(135deg, #ede9fe, #ddd6fe)"
            ]
            [ h2
                [ style "font-size" "2.5rem"
                , style "font-weight" "700"
                , style "margin" "0 0 16px"
                , style "color" "#4c1d95"
                ]
                [ text "View Timeline" ]
            , p
                [ style "font-size" "1.1rem"
                , style "color" "#6d28d9"
                , style "margin" "0"
                ]
                [ text "Scroll down - each card animates as it enters the viewport." ]
            ]

        -- Cards
        , div
            [ style "max-width" "700px"
            , style "margin" "0 auto"
            , style "padding" "clamp(24px, 6vmin, 60px) clamp(16px, 4.5vmin, 40px)"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "clamp(28px, 6vmin, 60px)"
            ]
            (List.map cardView cards)
        ]


type alias CardData =
    { animGroupName : String
    , color : String
    , label : String
    , title : String
    , body : String
    }


cards : List CardData
cards =
    [ { animGroupName = "view-card-1"
      , color = "#6366f1"
      , label = "01"
      , title = "Enter from below"
      , body = "Each card uses a ViewTimeline tied to itself as the scroll subject. As the card enters the viewport, it fades in and slides upward."
      }
    , { animGroupName = "view-card-2"
      , color = "#8b5cf6"
      , label = "02"
      , title = "Independent timelines"
      , body = "Every card has its own ViewTimeline. Animations are fully independent - each one triggers only when that specific card enters the viewport."
      }
    , { animGroupName = "view-card-3"
      , color = "#a78bfa"
      , label = "03"
      , title = "Range control"
      , body = "The rangeStart and rangeEnd functions define exactly when during the card's lifecycle the animation plays - entry, cover, contain, or exit."
      }
    , { animGroupName = "view-card-4"
      , color = "#7c3aed"
      , label = "04"
      , title = "Fire and forget"
      , body = "View timeline animations are fire-and-forget. No AnimState required - just trigger in your init and the browser handles the rest."
      }
    , { animGroupName = "view-card-5"
      , color = "#5b21b6"
      , label = "05"
      , title = "Composable builders"
      , body = "Combine opacity and translate in a single pipeline to create polished reveal effects with minimal code."
      }
    , { animGroupName = "view-card-6"
      , color = "#4c1d95"
      , label = "06"
      , title = "WAAPI powered"
      , body = "All animations run via the Web Animations API on the compositor thread - no JavaScript timers, no Elm subscriptions, maximum performance."
      }
    ]


cardView : CardData -> Html msg
cardView card =
    div
        (ViewTimeline.attributes card.animGroupName
            ++ [ style "display" "flex"
               , style "flex-direction" "column"
               , style "gap" "24px"
               , style "align-items" "flex-start"
               , style "padding" "clamp(18px, 4vmin, 32px)"
               , style "background" "white"
               , style "border-radius" "16px"
               , style "box-shadow" "0 4px 24px rgba(99,102,241,0.08)"
               , style "opacity" "0"
               ]
        )
        [ div
            [ style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "10px"
            , style "align-items" "flex-start"
            ]
            [ span
                [ style "font-size" "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
                [ style "flex" "1"
                , style "min-width" "0"
                , style "text-align" "left"
                ]
                [ h2
                    [ style "font-size" "1.3rem"
                    , style "font-weight" "700"
                    , style "margin" "0"
                    , style "color" "#111827"
                    , style "overflow-wrap" "break-word"
                    , style "word-break" "break-word"
                    ]
                    [ text card.title ]
                ]
            ]
        , p
            [ style "font-size" "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

Set rangeStart and rangeEnd to control when the animation begins and ends.

View Source Code
import Anim.Property.Opacity as Opacity


reveal : ViewTimeline.AnimBuilder eng -> ViewTimeline.AnimBuilder eng
reveal =
    ViewTimeline.rangeStart (ViewTimeline.Entry 0 ViewTimeline.Perc)
        >> ViewTimeline.rangeEnd (ViewTimeline.Entry 100 ViewTimeline.Perc)
        >> ViewTimeline.for "section"
        >> Opacity.begin
        >> Opacity.from 0
        >> Opacity.to 1
        >> Opacity.end

2. Render

Render attributes on the element being tracked by the view timeline.

View Source Code
view : Html Msg
view =
    section (ViewTimeline.attributes "section") [ text "Reveal me" ]

3. Trigger with animate

Call animate to send a fire-and-forget view-driven animation command.

View Source Code
port module Main exposing (main)

import Anim.Engine.ViewTimeline as ViewTimeline
import Json.Encode


port motionCmd : Json.Encode.Value -> Cmd msg


startReveal : Cmd Msg
startReveal =
    ViewTimeline.animate motionCmd reveal

4. Optional React

Subscribe only when you need lifecycle events in Elm.

View Source Code
import Json.Decode


port motionMsg : (Json.Decode.Value -> msg) -> Sub msg


type Msg
    = GotViewMsg ViewTimeline.AnimMsg


subscriptions : Model -> Sub Msg
subscriptions _ =
    ViewTimeline.subscriptions GotViewMsg motionMsg


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotViewMsg animMsg ->
            case ViewTimeline.update animMsg of
                Just (ViewTimeline.Ended _) ->
                    ( model, Cmd.none )

                _ ->
                    ( model, Cmd.none )

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

ViewTimeline 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
port module Main exposing (main)

import Json.Encode

port motionCmd : Json.Encode.Value -> Cmd msg

ViewTimeline.animate motionCmd scrollAnimation

πŸ“– See Triggering Animations for more info.

Update

Use update to process incoming messages and return a Maybe AnimEvent.

View Source Code
GotViewMsg animMsg ->
    case ViewTimeline.update animMsg of
        Just (ViewTimeline.Ended animGroup) ->
            handleAnimationEnd animGroup model

        Just (ViewTimeline.Iteration animGroup count) ->
            handleIteration animGroup count model

        _ ->
            ( model, Cmd.none )

Events

The ViewTimeline, ScrollTimeline 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:

  • Iteration includes the iteration count (Int)
  • AnimError carries an error string from the JavaScript layer
View Source Code
handleAnimEvent : Maybe ViewTimeline.AnimEvent -> Model -> ( Model, Cmd Msg )
handleAnimEvent maybeEvent model =
    case maybeEvent of
        Just (ViewTimeline.Ended "hero-card") ->
            ( model, Cmd.none )

        Just (ViewTimeline.Iteration "hero-card" count) ->
            ( model, Cmd.none )

        Just (ViewTimeline.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
port motionMsg : (Json.Decode.Value -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions _ =
    ViewTimeline.subscriptions GotViewMsg motionMsg

πŸ“– See React for more info.

View

Apply attributes to the animated element to attach the required animation group identifier.

View Source Code
div
    (ViewTimeline.attributes "hero-card")
    [ text "I animate as the user scrolls" ]

πŸ“– See Render for more info.

Axis

Vertical tracking is the default. Use horizontal in the animation pipeline when the element is inside a container that scrolls left and right.

View Source Code
ViewTimeline.animate motionCmd <|
    ViewTimeline.horizontal
        >> ViewTimeline.for "slide"
        >> Opacity.begin
        >> Opacity.from 0
        >> Opacity.to 1
        >> Opacity.end

Range

Setting the range determines when the animation starts and ends relative to the element's position in the viewport.

Use rangeStart and rangeEnd with Range constructor values. Both are optional β€” omitting them defaults to Cover 0 Perc through Cover 100 Perc.

View Source Code
ViewTimeline.animate motionCmd <|
    ViewTimeline.rangeStart (Entry 0 Perc)
        >> ViewTimeline.rangeEnd (Entry 100 Perc)
        >> ...
Constructor 0 is when… 100% / max is when…
Cover Element's leading edge first enters the viewport Element's trailing edge leaves the viewport
Contain Element is fully contained in the viewport Element is no longer fully contained in the viewport
Entry Element's leading edge first enters the viewport Element has fully entered the viewport
EntryCrossing Element's leading edge first enters the viewport Element has fully entered the viewport
Exit Element's leading edge starts to leave the viewport Element has fully left the viewport
ExitCrossing Element's leading edge starts to leave the viewport Element has fully left the viewport
Scroll Scroll container is at its very start Scroll container is at its very end

Try this interactive tool to see the different Ranges in action.

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
fadeIn =
    ViewTimeline.easing CubicInOut
        >> ViewTimeline.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.end

πŸ“– 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 view timeline.

View Source Code
bouncyReveal =
    ViewTimeline.spring Spring.wobbly
        >> ViewTimeline.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.end

πŸ“– See Spring for the full preset list and tuning guidance.

Discrete Properties

The ViewTimeline 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
import Anim.Extra.TransformOrder exposing (TransformProperty(..))

ViewTimeline.animate motionCmd <|
    ViewTimeline.for "slide"
        >> ViewTimeline.transformOrder [ Scale, Rotate, Translate ]
        >> Translate.begin
        >> ...

πŸ“– See Transform Order for full details.

When to Choose This Engine

Choose ViewTimeline when playback should follow how an element moves through the viewport.

  • Best for: section reveals, scroll storytelling, and enter/exit viewport 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
Range A position along the view timeline
Unit The unit for a range offset β€” Perc or Px
TransformProperty Custom transform ordering

Trigger

Function Type Description
animate (Value -> Cmd msg) -> (AnimBuilder eng -> AnimBuilder eng) -> Cmd msg Fire-and-forget view-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 viewport tracking

Range

Function Type Description
rangeStart Range -> AnimBuilder eng -> AnimBuilder eng Set when the animation begins
rangeEnd Range -> AnimBuilder eng -> AnimBuilder eng Set when the animation ends
Cover Float -> Unit -> Range Full element coverage β€” start or end
Contain Float -> Unit -> Range Full element containment β€” start or end
Entry Float -> Unit -> Range Element entering the viewport
EntryCrossing Float -> Unit -> Range Leading edge crossing
Exit Float -> Unit -> Range Element leaving the viewport
ExitCrossing Float -> Unit -> Range Trailing edge crossing
Scroll Float -> Unit -> Range Full scroll container range β€” start or end
Perc Unit Percentage unit
Px Unit Pixel unit

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.ViewTimeline documentation.

Next Steps

Get started with Properties.

Properties β†’