Skip to content

Animation Examples

Interactive examples demonstrating Elm Motion capabilities.

Each example uses the exact same animation for each of the Engines - quickly compare the behaviour of each Engine for different animation types.

Hello Text

Fades in text when the page loads. The obligatory "Hello" example.

View Example

Go to page


Pulsing Dot

A dot in the middle of the screen that pulses by scaling and fading in and out - looping forever and alternating direction on each iteration.

View Example

Go to page


Fade In/Out

Fade an element in and out with buttons.

View Examples

Go to page


Button Hovers

Three different hover effects.

View Examples

Note how animating Size causes browser reflow and repaint; as the button grows and shrinks, it affects the layout of surrounding elements. In contrast, Scale and Translate have no effect on the surrounding elements. More on this later.

Go to page


Interrupting Animations

Trigger an animation, then interrupt it mid-flight.

Single Property

View Examples

Behaviour: Smooth redirect from current mid-flight value to new end target value

Behaviour: The new @keyframes rules for the animation replace the existing rules.

📖 See: Keyframe Engine — Interrupting Animations for details.

Behaviour: Smooth redirect from current mid-flight value to new end target value

Behaviour: Smooth redirect from current mid-flight value to new end target value

Multiple Properties

View Examples

Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.

Behaviour: The new @keyframes rules for the animation replace the existing rules.

📖 See: Keyframe Engine — Interrupting Animations for details.

Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.

Behaviour: Translate and CustomColor (BackgroundColor) run independently side by side.

Multiple Axes

View Examples

Behaviour: Smooth redirect from current position

Behaviour: The new @keyframes rules for the animation replace the existing rules.

📖 See: Keyframe Engine — Interrupting Animations for details.

Behaviour: Seamless interruption to new target

Behaviour: Seamless interruption to new target

Freeze Axis

View Examples

Behaviour: Frozen axis holds its current position while the other axis animates to the new target

Behaviour: Frozen axis holds its current position while the other axis animates to the new target

Go to page


Controlling Animations

Control the ball animation with the buttons.

View Examples

Go to page


Discrete Properties

Show and hide elements with smooth fades using discrete CSS properties like display.

View Examples

Go to page


3D Animations

Rotating cube with expanding sides.

View Examples

Go to page


Transform Order

There are 6 boxes in the center, each one is triggered with the same animation, the only difference is the transform order.

View Examples

Go to page


Custom Properties

Border Radius

Animate border-radius using the generic Anim.Property.Custom module.

View Example

Go to page

Border Color

Animate border-color using the generic Anim.Property.CustomColor module.

View Example

Go to page

Sequencing

Sequence multiple animations in a single pipeline using staggered delays.

View Example

View Source Code
module Animation.Transition.Sequencing.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Transition as Transition
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Dict exposing (Dict)
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }



-- MODEL


type Direction
    = Forward
    | Backward


type alias Model =
    { animState : Transition.AnimState
    , direction : Maybe Direction
    }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Translate.initX rowOne 0
                , Translate.initX rowTwo 0
                , Translate.initX rowThree 0
                , Translate.initX rowFour 0
                ]
      , direction = Nothing
      }
    , Cmd.none
    )



-- ANIMATION


type alias MotionConfig =
    { delay : Int
    , speed : Float
    , easing : Easing
    }


globalConfig : MotionConfig
globalConfig =
    { delay = 1000
    , speed = 50
    , easing = BounceOut
    }


rowOne : String
rowOne =
    "rowOne"


rowOneConfig : MotionConfig
rowOneConfig =
    { delay = 0
    , speed = 85
    , easing = BackOut
    }


rowTwo : String
rowTwo =
    "rowTwo"


rowTwoConfig : MotionConfig
rowTwoConfig =
    { delay = 250
    , speed = 75
    , easing = ElasticOut
    }


rowThree : String
rowThree =
    "rowThree"


rowThreeConfig : MotionConfig
rowThreeConfig =
    { delay = 500
    , speed = 60
    , easing = QuadInOut
    }


rowFour : String
rowFour =
    "rowFour"


moveTo : Float -> AnimBuilder eng -> AnimBuilder eng
moveTo targetX =
    Translate.begin
        >> Translate.toX targetX
        >> Translate.end


animateRows : Float -> Transition.EngineBuilder -> Transition.EngineBuilder
animateRows targetX =
    Transition.delay globalConfig.delay
        >> Transition.speed globalConfig.speed
        >> Transition.easing globalConfig.easing
        >> Transition.cssUnitX Cqw
        >> Transition.for rowOne
        >> Transition.delay rowOneConfig.delay
        >> Transition.speed rowOneConfig.speed
        >> Transition.easing rowOneConfig.easing
        >> moveTo targetX
        >> Transition.for rowTwo
        >> Transition.delay rowTwoConfig.delay
        >> Transition.speed rowTwoConfig.speed
        >> Transition.easing rowTwoConfig.easing
        >> moveTo targetX
        >> Transition.for rowThree
        >> Transition.delay rowThreeConfig.delay
        >> Transition.speed rowThreeConfig.speed
        >> Transition.easing rowThreeConfig.easing
        >> moveTo targetX
        >> Transition.for rowFour
        >> moveTo targetX



-- UPDATE


type Msg
    = GotTransitionMsg Transition.AnimMsg
    | Toggle


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotTransitionMsg transitionMsg ->
            let
                ( newAnimState, _ ) =
                    Transition.update transitionMsg model.animState
            in
            ( { model | animState = newAnimState }
            , Cmd.none
            )

        Toggle ->
            let
                ( targetX, direction ) =
                    case model.direction of
                        Just Forward ->
                            ( 0, Just Backward )

                        _ ->
                            ( 90, Just Forward )
            in
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        animateRows targetX
                , direction = direction
              }
            , Cmd.none
            )



-- VIEW


rows : List String
rows =
    [ rowOne
    , rowTwo
    , rowThree
    , rowFour
    ]


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "height" "auto"
        , style "min-height" "360px"
        ]
        [ button
            [ class "ui-action-button primary"
            , onClick Toggle
            ]
            [ text "Animate Pipeline" ]
        , div
            [ style "width" "100%"
            , style "max-width" "560px"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "12px"
            , style "margin-top" "14px"
            ]
            (List.map (rowView model) rows)
        ]


rowView : Model -> String -> Html Msg
rowView model groupName =
    div
        [ style "display" "flex"
        , style "flex-wrap" "wrap"
        , style "align-items" "center"
        , style "gap" "8px"
        ]
        [ div
            [ style "position" "relative"
            , style "container-type" "size"
            , style "flex" "1"
            , style "height" "42px"
            , style "border" "1px solid #d4d4d8"
            , style "border-radius" "10px"
            , style "background" "#f8fafc"
            , style "overflow" "hidden"
            ]
            [ div
                (Transition.attributes groupName model.animState
                    ++ Transition.events GotTransitionMsg
                    ++ [ style "width" "10cqw"
                       , style "height" "100%"
                       , style "border-radius" "8px"
                       , style "background" "linear-gradient(135deg, #0ea5e9 0%, #22c55e 100%)"
                       ]
                )
                []
            ]
        ]
module Animation.Keyframe.Sequencing.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Keyframe as Keyframe
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Dict exposing (Dict)
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }



-- MODEL


type Direction
    = Forward
    | Backward


type alias Model =
    { animState : Keyframe.AnimState
    , direction : Maybe Direction
    }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Keyframe.init
                [ Translate.initX rowOne 0
                , Translate.initX rowTwo 0
                , Translate.initX rowThree 0
                , Translate.initX rowFour 0
                ]
      , direction = Nothing
      }
    , Cmd.none
    )



-- ANIMATION


type alias MotionConfig =
    { delay : Int
    , speed : Float
    , easing : Easing
    }


globalConfig : MotionConfig
globalConfig =
    { delay = 1000
    , speed = 50
    , easing = BounceOut
    }


rowOne : String
rowOne =
    "rowOne"


rowOneConfig : MotionConfig
rowOneConfig =
    { delay = 0
    , speed = 85
    , easing = BackOut
    }


rowTwo : String
rowTwo =
    "rowTwo"


rowTwoConfig : MotionConfig
rowTwoConfig =
    { delay = 250
    , speed = 75
    , easing = ElasticOut
    }


rowThree : String
rowThree =
    "rowThree"


rowThreeConfig : MotionConfig
rowThreeConfig =
    { delay = 500
    , speed = 60
    , easing = QuadInOut
    }


rowFour : String
rowFour =
    "rowFour"


moveTo : Float -> AnimBuilder eng -> AnimBuilder eng
moveTo targetX =
    Translate.begin
        >> Translate.toX targetX
        >> Translate.end


animateRows : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
animateRows targetX =
    Keyframe.delay globalConfig.delay
        >> Keyframe.speed globalConfig.speed
        >> Keyframe.easing globalConfig.easing
        >> Keyframe.cssUnitX Cqw
        >> Keyframe.for rowOne
        >> Keyframe.delay rowOneConfig.delay
        >> Keyframe.speed rowOneConfig.speed
        >> Keyframe.easing rowOneConfig.easing
        >> moveTo targetX
        >> Keyframe.for rowTwo
        >> Keyframe.delay rowTwoConfig.delay
        >> Keyframe.speed rowTwoConfig.speed
        >> Keyframe.easing rowTwoConfig.easing
        >> moveTo targetX
        >> Keyframe.for rowThree
        >> Keyframe.delay rowThreeConfig.delay
        >> Keyframe.speed rowThreeConfig.speed
        >> Keyframe.easing rowThreeConfig.easing
        >> moveTo targetX
        >> Keyframe.for rowFour
        >> moveTo targetX



-- UPDATE


type Msg
    = GotKeyframeMsg Keyframe.AnimMsg
    | Toggle


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotKeyframeMsg keyframeMsg ->
            let
                ( newAnimState, _ ) =
                    Keyframe.update keyframeMsg model.animState
            in
            ( { model | animState = newAnimState }
            , Cmd.none
            )

        Toggle ->
            let
                ( targetX, direction ) =
                    case model.direction of
                        Just Forward ->
                            ( 0, Just Backward )

                        _ ->
                            ( 90, Just Forward )
            in
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        animateRows targetX
                , direction = direction
              }
            , Cmd.none
            )



-- VIEW


rows : List String
rows =
    [ rowOne
    , rowTwo
    , rowThree
    , rowFour
    ]


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "height" "auto"
        , style "min-height" "360px"
        ]
        [ Keyframe.styleNode model.animState
        , button
            [ class "ui-action-button primary"
            , onClick Toggle
            ]
            [ text "Animate Pipeline" ]
        , div
            [ style "width" "100%"
            , style "max-width" "560px"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "12px"
            , style "margin-top" "14px"
            ]
            (List.map (rowView model) rows)
        ]


rowView : Model -> String -> Html Msg
rowView model groupName =
    div
        [ style "display" "flex"
        , style "flex-wrap" "wrap"
        , style "align-items" "center"
        , style "gap" "8px"
        ]
        [ div
            [ style "position" "relative"
            , style "container-type" "size"
            , style "flex" "1"
            , style "height" "42px"
            , style "border" "1px solid #d4d4d8"
            , style "border-radius" "10px"
            , style "background" "#f8fafc"
            ]
            [ div
                (Keyframe.attributes groupName model.animState
                    ++ Keyframe.events GotKeyframeMsg
                    ++ [ style "width" "10cqw"
                       , style "height" "100%"
                       , style "border-radius" "8px"
                       , style "background" "linear-gradient(135deg, #0ea5e9 0%, #22c55e 100%)"
                       ]
                )
                []
            ]
        ]
module Animation.Sub.Sequencing.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Sub as Sub
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Dict exposing (Dict)
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as Easing exposing (Easing(..))



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }



-- MODEL


type Direction
    = Forward
    | Backward


type alias Model =
    { animState : Sub.AnimState
    , direction : Maybe Direction
    }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Sub.init
                [ Translate.initX rowOne 0
                , Translate.initX rowTwo 0
                , Translate.initX rowThree 0
                , Translate.initX rowFour 0
                ]
      , direction = Nothing
      }
    , Cmd.none
    )



-- ANIMATION


type alias MotionConfig =
    { delay : Int
    , speed : Float
    , easing : Easing
    }


globalConfig : MotionConfig
globalConfig =
    { delay = 1000
    , speed = 50
    , easing = BounceOut
    }


rowOne : String
rowOne =
    "rowOne"


rowOneConfig : MotionConfig
rowOneConfig =
    { delay = 0
    , speed = 85
    , easing = BackOut
    }


rowTwo : String
rowTwo =
    "rowTwo"


rowTwoConfig : MotionConfig
rowTwoConfig =
    { delay = 250
    , speed = 75
    , easing = ElasticOut
    }


rowThree : String
rowThree =
    "rowThree"


rowThreeConfig : MotionConfig
rowThreeConfig =
    { delay = 500
    , speed = 60
    , easing = QuadInOut
    }


rowFour : String
rowFour =
    "rowFour"


moveTo : Float -> AnimBuilder eng -> AnimBuilder eng
moveTo targetX =
    Translate.begin
        >> Translate.toX targetX
        >> Translate.end


animateRows : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
animateRows targetX =
    Sub.delay globalConfig.delay
        >> Sub.speed globalConfig.speed
        >> Sub.easing globalConfig.easing
        >> Sub.cssUnitX Cqw
        >> Sub.for rowOne
        >> Sub.delay rowOneConfig.delay
        >> Sub.speed rowOneConfig.speed
        >> Sub.easing rowOneConfig.easing
        >> moveTo targetX
        >> Sub.for rowTwo
        >> Sub.delay rowTwoConfig.delay
        >> Sub.speed rowTwoConfig.speed
        >> Sub.easing rowTwoConfig.easing
        >> moveTo targetX
        >> Sub.for rowThree
        >> Sub.delay rowThreeConfig.delay
        >> Sub.speed rowThreeConfig.speed
        >> Sub.easing rowThreeConfig.easing
        >> moveTo targetX
        >> Sub.for rowFour
        >> moveTo targetX



-- UPDATE


type Msg
    = GotSubMsg Sub.AnimMsg
    | Toggle


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotSubMsg subMsg ->
            let
                ( newAnimState, _ ) =
                    Sub.update subMsg model.animState
            in
            ( { model | animState = newAnimState }
            , Cmd.none
            )

        Toggle ->
            let
                ( targetX, direction ) =
                    case model.direction of
                        Just Forward ->
                            ( 0, Just Backward )

                        _ ->
                            ( 90, Just Forward )
            in
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        animateRows targetX
                , direction = direction
              }
            , Cmd.none
            )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub.Sub Msg
subscriptions model =
    Sub.subscriptions GotSubMsg model.animState



-- VIEW


rows : List String
rows =
    [ rowOne
    , rowTwo
    , rowThree
    , rowFour
    ]


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "height" "auto"
        , style "min-height" "360px"
        ]
        [ button
            [ class "ui-action-button primary"
            , onClick Toggle
            ]
            [ text "Animate Pipeline" ]
        , div
            [ style "width" "100%"
            , style "max-width" "560px"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "12px"
            , style "margin-top" "14px"
            ]
            (List.map (rowView model) rows)
        ]


rowView : Model -> String -> Html Msg
rowView model groupName =
    div
        [ style "display" "flex"
        , style "flex-wrap" "wrap"
        , style "align-items" "center"
        , style "gap" "8px"
        ]
        [ div
            [ style "position" "relative"
            , style "container-type" "size"
            , style "flex" "1"
            , style "height" "42px"
            , style "border" "1px solid #d4d4d8"
            , style "border-radius" "10px"
            , style "background" "#f8fafc"
            ]
            [ div
                (Sub.attributes groupName model.animState
                    ++ [ style "width" "10cqw"
                       , style "height" "100%"
                       , style "border-radius" "8px"
                       , style "background" "linear-gradient(135deg, #0ea5e9 0%, #22c55e 100%)"
                       ]
                )
                []
            ]
        ]
port module Animation.WAAPI.Sequencing.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.WAAPI as WAAPI
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Dict exposing (Dict)
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))



-- PORTS


port motionCmd : Encode.Value -> Cmd msg


port motionMsg : (Encode.Value -> msg) -> Sub msg



-- MAIN


main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }



-- MODEL


type Direction
    = Forward
    | Backward


type alias Model =
    { animState : WAAPI.AnimState Msg
    , direction : Maybe Direction
    }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ Translate.initX rowOne 0
                , Translate.initX rowTwo 0
                , Translate.initX rowThree 0
                , Translate.initX rowFour 0
                ]
      , direction = Nothing
      }
    , Cmd.none
    )



-- ANIMATION


type alias MotionConfig =
    { delay : Int
    , speed : Float
    , easing : Easing
    }


globalConfig : MotionConfig
globalConfig =
    { delay = 1000
    , speed = 50
    , easing = BounceOut
    }


rowOne : String
rowOne =
    "rowOne"


rowOneConfig : MotionConfig
rowOneConfig =
    { delay = 0
    , speed = 85
    , easing = BackOut
    }


rowTwo : String
rowTwo =
    "rowTwo"


rowTwoConfig : MotionConfig
rowTwoConfig =
    { delay = 250
    , speed = 75
    , easing = ElasticOut
    }


rowThree : String
rowThree =
    "rowThree"


rowThreeConfig : MotionConfig
rowThreeConfig =
    { delay = 500
    , speed = 60
    , easing = QuadInOut
    }


rowFour : String
rowFour =
    "rowFour"


moveTo : Float -> AnimBuilder eng -> AnimBuilder eng
moveTo targetX =
    Translate.begin
        >> Translate.toX targetX
        >> Translate.end


animateRows : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
animateRows targetX =
    WAAPI.delay globalConfig.delay
        >> WAAPI.speed globalConfig.speed
        >> WAAPI.easing globalConfig.easing
        >> WAAPI.cssUnitX Cqw
        >> WAAPI.for rowOne
        >> WAAPI.delay rowOneConfig.delay
        >> WAAPI.speed rowOneConfig.speed
        >> WAAPI.easing rowOneConfig.easing
        >> moveTo targetX
        >> WAAPI.for rowTwo
        >> WAAPI.delay rowTwoConfig.delay
        >> WAAPI.speed rowTwoConfig.speed
        >> WAAPI.easing rowTwoConfig.easing
        >> moveTo targetX
        >> WAAPI.for rowThree
        >> WAAPI.delay rowThreeConfig.delay
        >> WAAPI.speed rowThreeConfig.speed
        >> WAAPI.easing rowThreeConfig.easing
        >> moveTo targetX
        >> WAAPI.for rowFour
        >> moveTo targetX



-- UPDATE


type Msg
    = GotWaapiMsg WAAPI.AnimMsg
    | Toggle


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotWaapiMsg waapiMsg ->
            let
                ( newAnimState, _ ) =
                    WAAPI.update waapiMsg model.animState
            in
            ( { model | animState = newAnimState }
            , Cmd.none
            )

        Toggle ->
            let
                ( targetX, direction ) =
                    case model.direction of
                        Just Forward ->
                            ( 0, Just Backward )

                        _ ->
                            ( 90, Just Forward )

                ( newAnimState, cmd ) =
                    WAAPI.animate model.animState <|
                        animateRows targetX
            in
            ( { model
                | animState = newAnimState
                , direction = direction
              }
            , cmd
            )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    WAAPI.subscriptions GotWaapiMsg model.animState



-- VIEW


rows : List String
rows =
    [ rowOne
    , rowTwo
    , rowThree
    , rowFour
    ]


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "height" "auto"
        , style "min-height" "360px"
        ]
        [ button
            [ class "ui-action-button primary"
            , onClick Toggle
            ]
            [ text "Animate Pipeline" ]
        , div
            [ style "width" "100%"
            , style "max-width" "560px"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "gap" "12px"
            , style "margin-top" "14px"
            ]
            (List.map (rowView model) rows)
        ]


rowView : Model -> String -> Html Msg
rowView model groupName =
    div
        [ style "display" "flex"
        , style "flex-wrap" "wrap"
        , style "align-items" "center"
        , style "gap" "8px"
        ]
        [ div
            [ style "position" "relative"
            , style "container-type" "size"
            , style "flex" "1"
            , style "min-width" "0"
            , style "height" "42px"
            , style "border" "1px solid #d4d4d8"
            , style "border-radius" "10px"
            , style "background" "#f8fafc"
            ]
            [ div
                (WAAPI.attributes groupName model.animState
                    ++ [ style "width" "10cqw"
                       , style "height" "100%"
                       , style "border-radius" "8px"
                       , style "background" "linear-gradient(135deg, #0ea5e9 0%, #22c55e 100%)"
                       ]
                )
                []
            ]
        ]

Timeline Engines

Scroll-driven animation examples using the dedicated timeline engines.

ScrollTimeline Engine

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 ]
            ]
        ]

ViewTimeline Engine

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 ]
        ]