Skip to content

Start Here

Timelines

Animations need a timeline to animate on, and modern Browsers have three: Document, Scroll and Viewport.

The Document timeline ties animations to time and is the one most folks use and maybe do so without knowing about it. If you've ever done CSS transition, keyframe or subscriptions (requestAnimationFrame) animations, you've used the Document timeline. Perhaps unsurprisingly then, the Transition, Keyframe, Sub and WAAPI Engines all animate on the Document timeline.

Introduced around July 2023 are the more recent additions of the Scroll and Viewport timelines. These tie animations to scroll position and viewport position respectively, not time, allowing animations to run in relation to scroll position as the user scrolls the document or container. The ScrollTimeline and ViewTimeline Engines target these new timelines.

Due to the Scroll and Viewport timelines being fairly recent additions, not all browsers may support them. At the time of writing, Firefox doesn't. This is not a problem though, because the JS companion automatically falls back to scroll-timeline-polyfill so your users always get the intended experience regardless of browser support.

Coding Style

The library codebase, and all the examples use function composition extensively.

New to function composition (>>)?

If you are more used to Elm's pipeline operator (|>), here's how they compare:

-- Using pipelines (|>)
fadeIn : AnimBuilder eng -> AnimBuilder eng
fadeIn animBuilder =
    animBuilder
        |> Opacity.begin
        |> Opacity.to 1
        |> Opacity.duration 5000
        |> Opacity.end

-- Using function composition (>>)
fadeIn : AnimBuilder eng -> AnimBuilder eng
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 5000
        >> Opacity.end

Both produce identical results. Because these builders are all functions of type AnimBuilder eng -> AnimBuilder eng, they compose naturally with >>. This codebase prefers the composition style because it keeps builder definitions concise and usually reads more cleanly than threading an explicit animBuilder through a pipeline.

The composition style works because each builder step is itself a partially-applied function of type AnimBuilder eng -> AnimBuilder eng - every argument except the builder has been supplied. >> then chains those partially-applied functions end-to-end into one larger function with the same AnimBuilder eng -> AnimBuilder eng shape.

Examples

Throughout the documentation you will find examples demonstrating features or concepts. The vast majority are for animations on the Document timeline, therefore all these examples will show the exact same animation for each of the Document Timeline Engines.

Responsive by default

All examples in this documentation are responsive - they adapt smoothly when the viewport is resized. If an engine tab is missing from a given example, it is because that engine either cannot express the animation at all, or cannot do so responsively. The remaining engine tabs show the same animation built with engines that can.

Here's a few examples to get started with.

1. Hello Text

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

View Example

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

import Anim.Engine.Transition as Transition exposing (AnimBuilder)
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Process
import Task



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Transition.AnimState }





init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )



-- ANIMATION
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end



-- UPDATE


type Msg
    = TriggerAnimation


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        TriggerAnimation ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for textLineOne
                            >> fadeIn
                            >> Transition.for dotOne
                            >> Transition.delay duration
                            >> fadeIn
                            >> Transition.for dotTwo
                            >> Transition.delay (duration * 2)
                            >> fadeIn
                            >> Transition.for dotThree
                            >> Transition.delay (duration * 3)
                            >> fadeIn
                            >> Transition.for textLineTwo
                            >> Transition.delay (duration * 4)
                            >> fadeIn
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "font-size" "clamp(28px, 10vw, 48px)"
        , style "font-weight" "bold"
        , style "text-align" "center"
        ]
        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Transition.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Transition.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Transition.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Transition.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Transition.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
module Animation.Keyframe.HelloText.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Keyframe as Keyframe
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Keyframe.AnimState }





init : ( Model, Cmd msg )
init =
    let
        animState =
            Keyframe.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Keyframe.animate animState <|
                Keyframe.for textLineOne
                    >> fadeIn
                    >> Keyframe.for dotOne
                    >> Keyframe.delay duration
                    >> fadeIn
                    >> Keyframe.for dotTwo
                    >> Keyframe.delay (duration * 2)
                    >> fadeIn
                    >> Keyframe.for dotThree
                    >> Keyframe.delay (duration * 3)
                    >> fadeIn
                    >> Keyframe.for textLineTwo
                    >> Keyframe.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )



-- ANIMATION
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end



-- VIEW


view : Model -> Html msg
view model =
    div
        [ class "example-stage"
        , style "font-size" "clamp(28px, 10vw, 48px)"
        , style "font-weight" "bold"
        , style "text-align" "center"
        ]
        [ Keyframe.styleNode model.animState
        , div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Keyframe.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Keyframe.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Keyframe.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
module Animation.Sub.HelloText.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Sub as Sub
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Sub.AnimState }





init : ( Model, Cmd Msg )
init =
    let
        animState =
            Sub.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Sub.animate animState <|
                Sub.for textLineOne
                    >> fadeIn
                    >> Sub.for dotOne
                    >> Sub.delay duration
                    >> fadeIn
                    >> Sub.for dotTwo
                    >> Sub.delay (duration * 2)
                    >> fadeIn
                    >> Sub.for dotThree
                    >> Sub.delay (duration * 3)
                    >> fadeIn
                    >> Sub.for textLineTwo
                    >> Sub.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )



-- ANIMATION
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end



-- UPDATE


type Msg
    = GotSubMsg Sub.AnimMsg


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



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "font-size" "clamp(28px, 10vw, 48px)"
        , style "font-weight" "bold"
        , style "text-align" "center"
        ]
        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Sub.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Sub.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Sub.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Sub.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Sub.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
port module Animation.WAAPI.HelloText.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.WAAPI as WAAPI
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, id, style)
import Json.Encode as Encode
import Process
import Task



-- PORTS
-- Outgoing Port


port motionCmd : Encode.Value -> Cmd msg



-- Incoming Port


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



-- MAIN


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



-- MODEL


type alias Model msg =
    { animState : WAAPI.AnimState msg }





init : ( Model msg, Cmd msg )
init =
    let
        animState =
            WAAPI.init motionCmd motionMsg <|
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]

        ( newAnimState, cmd ) =
            WAAPI.animate animState <|
                WAAPI.for textLineOne
                    >> fadeIn
                    >> WAAPI.for dotOne
                    >> WAAPI.delay duration
                    >> fadeIn
                    >> WAAPI.for dotTwo
                    >> WAAPI.delay (duration * 2)
                    >> fadeIn
                    >> WAAPI.for dotThree
                    >> WAAPI.delay (duration * 3)
                    >> fadeIn
                    >> WAAPI.for textLineTwo
                    >> WAAPI.delay (duration * 4)
                    >> fadeIn
    in
    ( { animState = newAnimState }
    , cmd
    )



-- ANIMATION
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end



-- VIEW


view : Model msg -> Html msg
view model =
    div
        [ class "example-stage"
        , style "font-size" "clamp(28px, 10vw, 48px)"
        , style "font-weight" "bold"
        , style "text-align" "center"
        ]
        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (WAAPI.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (WAAPI.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (WAAPI.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (WAAPI.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (WAAPI.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
Breaking It Down

1. Build

Animations are defined as functions that transform an AnimBuilder eng:

View Source Code
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end
-- Avoid typos from hardcoding strings in multiple places


duration : Int
duration =
    500


textLineOne : String
textLineOne =
    "textLineOne"


dotOne : String
dotOne =
    "dotOne"


dotTwo : String
dotTwo =
    "dotTwo"


dotThree : String
dotThree =
    "dotThree"


textLineTwo : String
textLineTwo =
    "textLineTwo"


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration duration
        >> Opacity.end

Notice how all the Engines use the exact same builder code 🎉

2. Initialize

Set up the initial state for your animated properties. This ensures elements render with the correct starting values before any animation runs:

View Source Code
type alias Model =
    { animState : Transition.AnimState }





init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )
type alias Model =
    { animState : Keyframe.AnimState }





init : ( Model, Cmd msg )
init =
    let
        animState =
            Keyframe.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Keyframe.animate animState <|
                Keyframe.for textLineOne
                    >> fadeIn
                    >> Keyframe.for dotOne
                    >> Keyframe.delay duration
                    >> fadeIn
                    >> Keyframe.for dotTwo
                    >> Keyframe.delay (duration * 2)
                    >> fadeIn
                    >> Keyframe.for dotThree
                    >> Keyframe.delay (duration * 3)
                    >> fadeIn
                    >> Keyframe.for textLineTwo
                    >> Keyframe.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )
type alias Model =
    { animState : Sub.AnimState }





init : ( Model, Cmd Msg )
init =
    let
        animState =
            Sub.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Sub.animate animState <|
                Sub.for textLineOne
                    >> fadeIn
                    >> Sub.for dotOne
                    >> Sub.delay duration
                    >> fadeIn
                    >> Sub.for dotTwo
                    >> Sub.delay (duration * 2)
                    >> fadeIn
                    >> Sub.for dotThree
                    >> Sub.delay (duration * 3)
                    >> fadeIn
                    >> Sub.for textLineTwo
                    >> Sub.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )
type alias Model msg =
    { animState : WAAPI.AnimState msg }





init : ( Model msg, Cmd msg )
init =
    let
        animState =
            WAAPI.init motionCmd motionMsg <|
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]

        ( newAnimState, cmd ) =
            WAAPI.animate animState <|
                WAAPI.for textLineOne
                    >> fadeIn
                    >> WAAPI.for dotOne
                    >> WAAPI.delay duration
                    >> fadeIn
                    >> WAAPI.for dotTwo
                    >> WAAPI.delay (duration * 2)
                    >> fadeIn
                    >> WAAPI.for dotThree
                    >> WAAPI.delay (duration * 3)
                    >> fadeIn
                    >> WAAPI.for textLineTwo
                    >> WAAPI.delay (duration * 4)
                    >> fadeIn
    in
    ( { animState = newAnimState }
    , cmd
    )

The WAAPI Engine also requires both it's port functions (motionCmd & motionMsg).

📖 See WAAPI Engine - Define Ports in Elm for more info.

3. Render

Use the attributes function to apply the animation's attributes to your element:

View Source Code
        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Transition.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Transition.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Transition.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Transition.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Transition.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
        [ Keyframe.styleNode model.animState
        , div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Keyframe.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Keyframe.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Keyframe.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]

Keyframe animations also need a styleNode with the keyframe rules.

📖 See Keyframe Style Node for more info.

        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Sub.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Sub.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Sub.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Sub.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Sub.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
        [ div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (WAAPI.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (WAAPI.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (WAAPI.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (WAAPI.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (WAAPI.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]

Exactly what attributes returns depends on the Engine being used, the animation configuration and the current animation state.

4. Trigger

Engines trigger their animations with their animate function.

View Source Code
init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )

Process.sleep 0 is used to trigger the animation immediately after first render; this allows the browser to compute the starting values for the transition.

The animation is then triggered in update.

        TriggerAnimation ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for textLineOne
                            >> fadeIn
                            >> Transition.for dotOne
                            >> Transition.delay duration
                            >> fadeIn
                            >> Transition.for dotTwo
                            >> Transition.delay (duration * 2)
                            >> fadeIn
                            >> Transition.for dotThree
                            >> Transition.delay (duration * 3)
                            >> fadeIn
                            >> Transition.for textLineTwo
                            >> Transition.delay (duration * 4)
                            >> fadeIn
              }
            , Cmd.none
            )
📖 See Transition Engine - How CSS Transitions Work for more info.

init : ( Model, Cmd msg )
init =
    let
        animState =
            Keyframe.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Keyframe.animate animState <|
                Keyframe.for textLineOne
                    >> fadeIn
                    >> Keyframe.for dotOne
                    >> Keyframe.delay duration
                    >> fadeIn
                    >> Keyframe.for dotTwo
                    >> Keyframe.delay (duration * 2)
                    >> fadeIn
                    >> Keyframe.for dotThree
                    >> Keyframe.delay (duration * 3)
                    >> fadeIn
                    >> Keyframe.for textLineTwo
                    >> Keyframe.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )

Keyframe animations can be triggered in your module's init function - the @keyframes rules are added to the DOM ready for first render providing you add the styleNode to your view:

        [ Keyframe.styleNode model.animState
        , div
            [ style "display" "flex"
            , style "justify-content" "center"
            , style "align-items" "center"
            , style "gap" "0.25em"
            ]
            [ div
                (Keyframe.attributes textLineOne model.animState ++ [])
                [ text "Elm Motion says" ]
            , div
                (Keyframe.attributes dotOne model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotTwo model.animState
                    ++ []
                )
                [ text "." ]
            , div
                (Keyframe.attributes dotThree model.animState
                    ++ []
                )
                [ text "." ]
            ]
        , div
            (Keyframe.attributes textLineTwo model.animState
                ++ [ style "width" "100%" ]
            )
            [ text "Hello World!" ]
        ]
init : ( Model, Cmd Msg )
init =
    let
        animState =
            Sub.init
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]
    in
    ( { animState =
            Sub.animate animState <|
                Sub.for textLineOne
                    >> fadeIn
                    >> Sub.for dotOne
                    >> Sub.delay duration
                    >> fadeIn
                    >> Sub.for dotTwo
                    >> Sub.delay (duration * 2)
                    >> fadeIn
                    >> Sub.for dotThree
                    >> Sub.delay (duration * 3)
                    >> fadeIn
                    >> Sub.for textLineTwo
                    >> Sub.delay (duration * 4)
                    >> fadeIn
      }
    , Cmd.none
    )

The Sub Engine can be triggered from your module's init function - the animation starts on the first update loop.

init : ( Model msg, Cmd msg )
init =
    let
        animState =
            WAAPI.init motionCmd motionMsg <|
                [ Opacity.init textLineOne 0
                , Opacity.init dotOne 0
                , Opacity.init dotTwo 0
                , Opacity.init dotThree 0
                , Opacity.init textLineTwo 0
                ]

        ( newAnimState, cmd ) =
            WAAPI.animate animState <|
                WAAPI.for textLineOne
                    >> fadeIn
                    >> WAAPI.for dotOne
                    >> WAAPI.delay duration
                    >> fadeIn
                    >> WAAPI.for dotTwo
                    >> WAAPI.delay (duration * 2)
                    >> fadeIn
                    >> WAAPI.for dotThree
                    >> WAAPI.delay (duration * 3)
                    >> fadeIn
                    >> WAAPI.for textLineTwo
                    >> WAAPI.delay (duration * 4)
                    >> fadeIn
    in
    ( { animState = newAnimState }
    , cmd
    )

The WAAPI Engine also returns a Cmd from animate that sends the animation data to the Javascript Companion. The Cmd is sent immediately after first render, the JS companion starts the animation immediately that it is received.

5. Update

Keep the Engine's state updated to make use of state-tracked features.

For the Transition, Keyframe and WAAPI engines, update is not required for this example; for the Sub Engine, update is always required.

View Source Code

Not required for this animation.


Not required for this animation.

type Msg
    = GotSubMsg Sub.AnimMsg


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



-- SUBSCRIPTIONS


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

Always required.


Not required for this animation.


2. Toggle Visibility

Fade an element in and out with buttons.

View Examples

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

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Transition as Transition
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Transition.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )



-- ANIMATION


animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0



-- UPDATE


type Msg
    = TriggerFadeIn
    | TriggerFadeOut


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        TriggerFadeIn ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div [ class "example-stage" ]
        [ div [ class "example-controls" ]
            [ button
                [ onClick TriggerFadeIn
                , class "ui-action-button primary"
                ]
                [ text "Fade In" ]
            , button
                [ onClick TriggerFadeOut
                , class "ui-action-button primary"
                ]
                [ text "Fade Out" ]
            ]

        , div
            (Transition.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

        ]
module Animation.Keyframe.FadeInOut.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Keyframe as Keyframe
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, id, style)
import Html.Events exposing (onClick)



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Keyframe.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Keyframe.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )



-- ANIMATION


animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0



-- UPDATE


type Msg
    = TriggerFadeIn
    | TriggerFadeOut


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        TriggerFadeIn ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage" ]
        [ div [ class "example-controls" ]
            [ button
                [ onClick TriggerFadeIn
                , class "ui-action-button primary"
                ]
                [ text "Fade In" ]
            , button
                [ onClick TriggerFadeOut
                , class "ui-action-button primary"
                ]
                [ text "Fade Out" ]
            ]

        , Keyframe.styleNode model.animState
        , div
            (Keyframe.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

        ]
module Animation.Sub.FadeInOut.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Sub as Sub
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (class, id, style)
import Html.Events exposing (onClick)



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Sub.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Sub.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )



-- ANIMATION


animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0



-- UPDATE


type Msg
    = GotSubMsg Sub.AnimMsg
    | TriggerFadeIn
    | TriggerFadeOut





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 )

        TriggerFadeIn ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "text-align" "center"
        ]
        [ div [ class "example-controls" ]
            [ button
                [ onClick TriggerFadeIn
                , class "ui-action-button primary"
                ]
                [ text "Fade In" ]
            , button
                [ onClick TriggerFadeOut
                , class "ui-action-button primary"
                ]
                [ text "Fade Out" ]
            ]

        , div
            (Sub.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

        ]
port module Animation.WAAPI.FadeInOut.Main exposing (main)

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



-- 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 alias Model =
    { animState : WAAPI.AnimState Msg }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )



-- ANIMATION


animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0



-- UPDATE


type Msg
    = GotWaapiMsg WAAPI.AnimMsg
    | TriggerFadeIn
    | TriggerFadeOut





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 )

        TriggerFadeIn ->
            let
                ( newAnimState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for animGroup
                            >> fadeIn
            in
            ( { model | animState = newAnimState }, cmd )

        TriggerFadeOut ->
            let
                ( newAnimState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for animGroup
                            >> fadeOut
            in
            ( { model | animState = newAnimState }, cmd )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        ]
        [ div
            [ class "example-controls" ]
            [ button
                [ onClick TriggerFadeIn
                , class "ui-action-button primary"
                ]
                [ text "Fade In" ]
            , button
                [ onClick TriggerFadeOut
                , class "ui-action-button primary"
                ]
                [ text "Fade Out" ]
            ]
          div
            (WAAPI.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

        ]
Breaking It Down

1. Build

Animations are defined as functions that transform an AnimBuilder eng:

View Source Code
animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0
animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0
animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0
animGroup : String
animGroup =
    "fadeAnim"


fadeTo : Float -> AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeTo to =
    Opacity.begin
        >> Opacity.to to
        >> Opacity.duration 2500
        >> Opacity.end


fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
    fadeTo 1


fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
    fadeTo 0

2. Initialize

Set up the initial state for your animated properties. This ensures elements render with the correct starting values before any animation runs:

View Source Code
type alias Model =
    { animState : Transition.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : Keyframe.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Keyframe.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : Sub.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Sub.init
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : WAAPI.AnimState Msg }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ Opacity.init animGroup 0 ]
      }
    , Cmd.none
    )

The WAAPI Engine also requires both it's port functions (motionCmd & motionMsg).

📖 See WAAPI Engine - Define Ports in Elm for more info.

Here, we initialize the opacity to 0 so the element starts invisible.

3. Render

Use the attributes function to apply the animation's attributes to your element:

View Source Code
        , div
            (Transition.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []
        , Keyframe.styleNode model.animState
        , div
            (Keyframe.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

Keyframe animations also need a styleNode with the keyframe rules.

📖 See Keyframe Style Node for more info.

        , div
            (Sub.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []
          div
            (WAAPI.attributes animGroup model.animState
                ++ [ style "background-color" "red"
                   , style "border-radius" "8px"
                   , style "width" "100%"
                   , style "flex" "1 1 auto"
                   , style "min-height" "0"
                   ]
            )
            []

Exactly what attributes returns depends on the Engine being used, the animation configuration and the current animation state.

4. Trigger

Engines trigger their animations with their animate function.

View Source Code
        TriggerFadeIn ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )
        TriggerFadeIn ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )
        TriggerFadeIn ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for animGroup
                            >> fadeIn
              }
            , Cmd.none
            )

        TriggerFadeOut ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for animGroup
                            >> fadeOut
              }
            , Cmd.none
            )
        TriggerFadeIn ->
            let
                ( newAnimState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for animGroup
                            >> fadeIn
            in
            ( { model | animState = newAnimState }, cmd )

        TriggerFadeOut ->
            let
                ( newAnimState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for animGroup
                            >> fadeOut
            in
            ( { model | animState = newAnimState }, cmd )

The WAAPI Engine also returns a Cmd from animate that sends the animation data to the Javascript Companion.

5. Update

Keep the Engine's state updated to make use of state-tracked features.

For the Transition and Keyframe engines, update is not required for this example; for the Sub and WAAPI engines, update is required.

View Source Code

Not required for this animation.


Not required for this animation.

type Msg
    = GotSubMsg Sub.AnimMsg


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 )



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

Always required.

type Msg
    = GotWaapiMsg WAAPI.AnimMsg


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 )



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

Required for this example so WAAPI property updates stay in sync - without it, mid-flight interruptions won't work correctly.


3. Interactive Hover Effects

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.

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

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Transition as Transition
import Anim.Extra.View3D as View3D
import Anim.Property.Scale as Scale
import Anim.Property.Size as Size
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events.Extra.Pointer as Pointer
import Motion.Easing exposing (Easing(..))



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Transition.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )



-- ANIMATIONS
-- Avoid typos from hardcoding strings in multiple places


scaleButton : String
scaleButton =
    "scaleButton"


sizeButton : String
sizeButton =
    "sizeButton"


zButton : String
zButton =
    "zButton"


baseWidth : Float
baseWidth =
    51


baseHeight : Float
baseHeight =
    15.8


hoverWidth : Float
hoverWidth =
    60


hoverHeight : Float
hoverHeight =
    20


hoverDuration : Int
hoverDuration =
    200


hoverEasing : Easing
hoverEasing =
    CubicOut


unhoverEasing : Easing
unhoverEasing =
    CubicIn





scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end



-- UPDATE


type Msg
    = ScaleHover
    | ScaleUnhover
    | SizeHover
    | SizeUnhover
    | ZHover
    | ZUnhover


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ScaleHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for zButton
                            >> setDown
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "container-type" "size"
        ]
        [ div
            [ style "padding" "7px"
            , style "border-radius" "12px"
            , style "border" "2px solid #041e53"
            , style "justify-content" "center"
            , style "gap" "clamp(12px, 3vmin, 24px)"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "align-items" "center"
            ]
            [ button "Scale" ScaleHover ScaleUnhover scaleButton model.animState
            , button "Size" SizeHover SizeUnhover sizeButton model.animState
            , div
                [ View3D.perspective 600 ]
                [ button "Translate Z" ZHover ZUnhover zButton model.animState ]
            ]
        ]





button : String -> Msg -> Msg -> String -> Transition.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Transition.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
module Animation.Keyframe.ButtonHovers.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Keyframe as Keyframe
import Anim.Extra.View3D as View3D
import Anim.Property.Scale as Scale
import Anim.Property.Size as Size
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events.Extra.Pointer as Pointer
import Motion.Easing exposing (Easing(..))



-- MAIN


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



-- MODEL


type alias Model =
    { animState : Keyframe.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Keyframe.init
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )



-- ANIMATIONS
-- Avoid typos from hardcoding strings in multiple places


scaleButton : String
scaleButton =
    "scaleButton"


sizeButton : String
sizeButton =
    "sizeButton"


zButton : String
zButton =
    "zButton"


baseWidth : Float
baseWidth =
    51


baseHeight : Float
baseHeight =
    15.8


hoverWidth : Float
hoverWidth =
    60


hoverHeight : Float
hoverHeight =
    20


hoverDuration : Int
hoverDuration =
    200


hoverEasing : Easing
hoverEasing =
    CubicOut


unhoverEasing : Easing
unhoverEasing =
    CubicIn





scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end



-- UPDATE


type Msg
    = ScaleHover
    | ScaleUnhover
    | SizeHover
    | SizeUnhover
    | ZHover
    | ZUnhover


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ScaleHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for zButton
                            >> setDown
              }
            , Cmd.none
            )



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "container-type" "size"
        ]
        [ Keyframe.styleNode model.animState
        , div
            [ style "padding" "7px"
            , style "border-radius" "12px"
            , style "border" "2px solid #041e53"
            , style "justify-content" "center"
            , style "gap" "clamp(12px, 3vmin, 24px)"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "align-items" "center"
            ]
            [ button "Scale" ScaleHover ScaleUnhover scaleButton model.animState
            , button "Size" SizeHover SizeUnhover sizeButton model.animState
            , div
                [ View3D.perspective 600 ]
                [ button "Translate Z" ZHover ZUnhover zButton model.animState ]
            ]
        ]





button : String -> Msg -> Msg -> String -> Keyframe.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Keyframe.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
module Animation.Sub.ButtonHovers.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.Sub as Sub
import Anim.Extra.View3D as View3D
import Anim.Property.Scale as Scale
import Anim.Property.Size as Size
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events.Extra.Pointer as Pointer
import Json.Decode as Decode exposing (Decoder)
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 alias Model =
    { animState : Sub.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Sub.init
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )



-- ANIMATIONS
-- Avoid typos from hardcoding strings in multiple places


scaleButton : String
scaleButton =
    "scaleButton"


sizeButton : String
sizeButton =
    "sizeButton"


zButton : String
zButton =
    "zButton"


baseWidth : Float
baseWidth =
    51


baseHeight : Float
baseHeight =
    15.8


hoverWidth : Float
hoverWidth =
    60


hoverHeight : Float
hoverHeight =
    20


hoverDuration : Int
hoverDuration =
    200


hoverEasing : Easing
hoverEasing =
    CubicOut


unhoverEasing : Easing
unhoverEasing =
    CubicIn





scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end



-- UPDATE


type Msg
    = GotSubMsg Sub.AnimMsg
    | ScaleHover
    | ScaleUnhover
    | SizeHover
    | SizeUnhover
    | ZHover
    | ZUnhover





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

        ScaleHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for zButton
                            >> setDown
              }
            , Cmd.none
            )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "container-type" "size"
        ]
        [ div
            [ style "padding" "7px"
            , style "border-radius" "12px"
            , style "border" "2px solid #041e53"
            , style "justify-content" "center"
            , style "gap" "clamp(12px, 3vmin, 24px)"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "align-items" "center"
            ]
            [ button "Scale" ScaleHover ScaleUnhover scaleButton model.animState
            , button "Size" SizeHover SizeUnhover sizeButton model.animState
            , div
                [ View3D.perspective 600 ]
                [ button "Translate Z" ZHover ZUnhover zButton model.animState ]
            ]
        ]





button : String -> Msg -> Msg -> String -> Sub.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Sub.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
port module Animation.WAAPI.ButtonHovers.Main exposing (main)

import Anim.Builder exposing (AnimBuilder)
import Anim.Engine.WAAPI as WAAPI
import Anim.Extra.View3D as View3D
import Anim.Property.Scale as Scale
import Anim.Property.Size as Size
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, style)
import Html.Events.Extra.Pointer as Pointer
import Json.Encode as Encode
import Motion.Easing exposing (Easing(..))



-- MAIN


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



-- PORTS


port motionCmd : Encode.Value -> Cmd msg


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



-- MODEL


type alias Model =
    { animState : WAAPI.AnimState Msg }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )



-- ANIMATIONS
-- Avoid typos from hardcoding strings in multiple places


scaleButton : String
scaleButton =
    "scaleButton"


sizeButton : String
sizeButton =
    "sizeButton"


zButton : String
zButton =
    "zButton"


baseWidth : Float
baseWidth =
    51


baseHeight : Float
baseHeight =
    15.8


hoverWidth : Float
hoverWidth =
    60


hoverHeight : Float
hoverHeight =
    20


hoverDuration : Int
hoverDuration =
    200


hoverEasing : Easing
hoverEasing =
    CubicOut


unhoverEasing : Easing
unhoverEasing =
    CubicIn





scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end



-- UPDATE


type Msg
    = GotWaapiMsg WAAPI.AnimMsg
    | ScaleHover
    | ScaleUnhover
    | SizeHover
    | SizeUnhover
    | ZHover
    | ZUnhover





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

        ScaleHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for scaleButton
                            >> scaleUp
            in
            ( { model | animState = animState }, cmd )

        ScaleUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for scaleButton
                            >> scaleDown
            in
            ( { model | animState = animState }, cmd )

        SizeHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for sizeButton
                            >> growSize
            in
            ( { model | animState = animState }, cmd )

        SizeUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for sizeButton
                            >> shrinkSize
            in
            ( { model | animState = animState }, cmd )

        ZHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for zButton
                            >> liftUp
            in
            ( { model | animState = animState }, cmd )

        ZUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for zButton
                            >> setDown
            in
            ( { model | animState = animState }, cmd )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Html Msg
view model =
    div
        [ class "example-stage"
        , style "container-type" "size"
        ]
        [ div
            [ style "padding" "7px"
            , style "border-radius" "12px"
            , style "border" "2px solid #041e53"
            , style "justify-content" "center"
            , style "gap" "clamp(12px, 3vmin, 24px)"
            , style "display" "flex"
            , style "flex-direction" "column"
            , style "align-items" "center"
            ]
            [ button "Scale" ScaleHover ScaleUnhover scaleButton model.animState
            , button "Size" SizeHover SizeUnhover sizeButton model.animState
            , div
                [ View3D.perspective 600 ]
                [ button "Translate Z" ZHover ZUnhover zButton model.animState ]
            ]
        ]





button : String -> Msg -> Msg -> String -> WAAPI.AnimState Msg -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (WAAPI.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
Breaking It Down

1. Build

Animations are defined as functions that transform an AnimBuilder eng:

View Source Code
scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end
scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end
scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end
scaleUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleUp =
    Scale.begin
        >> Scale.to 1.1
        >> Scale.duration hoverDuration
        >> Scale.easing hoverEasing
        >> Scale.end


scaleDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
scaleDown =
    Scale.begin
        >> Scale.to 1
        >> Scale.duration hoverDuration
        >> Scale.easing unhoverEasing
        >> Scale.end


growSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
growSize =
    Size.begin
        >> Size.toHW hoverHeight hoverWidth
        >> Size.duration hoverDuration
        >> Size.easing hoverEasing
        >> Size.end


shrinkSize : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
shrinkSize =
    Size.begin
        >> Size.toHW baseHeight baseWidth
        >> Size.duration hoverDuration
        >> Size.easing unhoverEasing
        >> Size.end


liftUp : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
liftUp =
    Translate.begin
        >> Translate.toZ 60
        >> Translate.duration hoverDuration
        >> Translate.easing hoverEasing
        >> Translate.end


setDown : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
setDown =
    Translate.begin
        >> Translate.toZ 0
        >> Translate.duration hoverDuration
        >> Translate.easing unhoverEasing
        >> Translate.end

2. Initialize

Set up the initial state for your animated properties. This ensures elements render with the correct starting values before any animation runs:

View Source Code
type alias Model =
    { animState : Transition.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Transition.init
                [ Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : Keyframe.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Keyframe.init
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : Sub.AnimState }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            Sub.init
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )
type alias Model =
    { animState : WAAPI.AnimState Msg }


init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ Size.initHW sizeButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW scaleButton baseHeight baseWidth >> Size.cssUnit Cqmin
                , Size.initHW zButton baseHeight baseWidth >> Size.cssUnit Cqmin
                ]
      }
    , Cmd.none
    )

3. Render

Use the attributes function to apply the animation's attributes to your element:

View Source Code
button : String -> Msg -> Msg -> String -> Transition.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Transition.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
button : String -> Msg -> Msg -> String -> Keyframe.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Keyframe.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]

Keyframe animations also need a styleNode with the keyframe rules.

📖 See Keyframe Style Node for more info.

button : String -> Msg -> Msg -> String -> Sub.AnimState -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (Sub.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]
button : String -> Msg -> Msg -> String -> WAAPI.AnimState Msg -> Html Msg
button label hoverMsg unhoverMsg groupName animState =
    div
        (WAAPI.attributes groupName animState
            ++ [ Pointer.onEnter (\_ -> hoverMsg)
               , Pointer.onLeave (\_ -> unhoverMsg)
               , style "display" "flex"
               , style "align-items" "center"
               , style "justify-content" "center"
               , style "background-color" "#3b82f6"
               , style "color" "white"
               , style "font-size" "clamp(14px, 3.5cqw, 26px)"
               , style "font-weight" "600"
               , style "padding" "0 clamp(8px, 2.2cqmin, 16px)"
               , style "border-radius" "8px"
               , style "cursor" "pointer"
               , style "touch-action" "manipulation"
               , style "-webkit-tap-highlight-color" "transparent"
               , style "user-select" "none"
               , style "box-sizing" "border-box"
               , style "box-shadow" "0 3px 5px rgba(0, 0, 0, 0.5), 0 1px 3px rgba(0, 0, 0, 0.4)"
               ]
        )
        [ text label ]

Exactly what attributes returns depends on the Engine being used, the animation configuration and the current animation state.

4. Trigger

Engines trigger their animations with their animate function.

View Source Code
        ScaleHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Transition.animate model.animState <|
                        Transition.for zButton
                            >> setDown
              }
            , Cmd.none
            )
        ScaleHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Keyframe.animate model.animState <|
                        Keyframe.for zButton
                            >> setDown
              }
            , Cmd.none
            )
        ScaleHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for scaleButton
                            >> scaleUp
              }
            , Cmd.none
            )

        ScaleUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for scaleButton
                            >> scaleDown
              }
            , Cmd.none
            )

        SizeHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for sizeButton
                            >> growSize
              }
            , Cmd.none
            )

        SizeUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for sizeButton
                            >> shrinkSize
              }
            , Cmd.none
            )

        ZHover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for zButton
                            >> liftUp
              }
            , Cmd.none
            )

        ZUnhover ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        Sub.for zButton
                            >> setDown
              }
            , Cmd.none
            )
        ScaleHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for scaleButton
                            >> scaleUp
            in
            ( { model | animState = animState }, cmd )

        ScaleUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for scaleButton
                            >> scaleDown
            in
            ( { model | animState = animState }, cmd )

        SizeHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for sizeButton
                            >> growSize
            in
            ( { model | animState = animState }, cmd )

        SizeUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for sizeButton
                            >> shrinkSize
            in
            ( { model | animState = animState }, cmd )

        ZHover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for zButton
                            >> liftUp
            in
            ( { model | animState = animState }, cmd )

        ZUnhover ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        WAAPI.for zButton
                            >> setDown
            in
            ( { model | animState = animState }, cmd )

5. Update

Keep the Engine's state updated to make use of state-tracked features.

For the Transition and Keyframe engines, update is not required for this example; for the Sub and WAAPI engines, update is required.

View Source Code

Not required for this animation.


Not required for this animation.

type Msg
    = GotSubMsg Sub.AnimMsg


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



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

Always required.

type Msg
    = GotWaapiMsg WAAPI.AnimMsg


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



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

Required for this interactive hover example so WAAPI property updates stay in sync - without it, mid-flight interruptions won't work correctly.

Next Steps

Now that you can create a simple animation, continue with the animation workflow.

Animation Workflow →