Skip to content

WAAPI Engine

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

The WAAPI Engine uses the Web Animations API via Elm ports and a JavaScript companion. It combines browser-native performance with programmatic control.

Example

3D animation - rotating cube with expanding sides.

View Example

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

import Anim.Builder exposing (AnimBuilder, ForWAAPI)
import Anim.Engine.WAAPI as WAAPI
import Anim.Extra.View3D as View3D
import Anim.Property.Rotate as Rotate
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser exposing (Document)
import Html exposing (Html, div, text)
import Html.Attributes exposing (id, style)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
import Process
import Task



-- PORTS


port motionCmd : Encode.Value -> Cmd msg


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



-- MAIN


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



-- MODEL


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





init : ( Model, Cmd Msg )
init =
    let
        initialAnimState =
            WAAPI.init motionCmd motionMsg <|
                [ -- Bring the cube forward on the Z axis
                  -- so that it doesn't get clipped by the
                  -- z=0 clipping plane when we expand the
                  -- sides and rotate. This is a fixed 3D depth
                  -- offset, unrelated to layout, so it stays
                  -- in pixels.
                  Translate.initZ cubeGroupName 200 >> Translate.cssUnit Px

                -- Position each face in 3D space along the axis it faces.
                -- Face offsets use `Cqmin` so the cube scales proportionally
                -- with the stage's new dimensions on resize.
                -- Front/Back faces move on Z (forward/backward)
                -- Left/Right faces move on X (sideways)
                -- Top/Bottom faces move on Y (up/down)
                , Translate.initZ frontFace.groupName depth >> Translate.cssUnit Cqmin
                , Translate.initZ backFace.groupName (depth * -1)
                    >> Translate.cssUnit Cqmin
                    -- Rotate each face into position to build the cube
                    -- Front face is not rotated due to facing forward by default
                    >> Rotate.initY backFace.groupName 180
                , Translate.initX rightFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY rightFace.groupName 90
                , Translate.initX leftFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY leftFace.groupName -90
                , Translate.initY topFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX topFace.groupName 90
                , Translate.initY bottomFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX bottomFace.groupName -90

                -- The text labels all start on the same plane as their faces
                -- at z=0, which is the default starting value, so we don't need
                -- to initialize them
                ]
    in
    ( { animState = initialAnimState
      , state = Opening
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )





type State
    = Opening
    | Closing
    | RotatingOpen
    | RotatingClosed



-- Cube configuration


cubeGroupName : String
cubeGroupName =
    "cubeAnim"


{-| Cube edge length expressed in `Cqmin` (% of the stage's smaller
dimension). The stage applies `container-type: size`, so the cube
resizes automatically with the viewport without any Elm-side
measurement.
-}
cubeSize : Float
cubeSize =
    18


cubeSizeCss : String
cubeSizeCss =
    String.fromFloat cubeSize ++ "cqmin"


{-| Distance each face sits in front of / behind the cube centre.
Half the cube edge so adjacent faces meet at the cube corners.
-}
depth : Float
depth =
    cubeSize / 2



-- Face configuration


type alias TextConfig =
    { id : String
    , groupName : String
    , label : String
    , color : String
    }


type alias FaceConfig =
    { id : String
    , groupName : String
    , label : String
    , background : String
    , borderColor : String
    , text : TextConfig
    }


frontFace : FaceConfig
frontFace =
    { id = "front-face"
    , groupName = "frontFaceAnim"
    , label = "FRONT"
    , background = "rgb(52, 152, 219)"
    , borderColor = "rgb(41, 128, 185)"
    , text =
        { id = "front-face-text"
        , groupName = "frontFaceTextAnim"
        , label = "FRONT"
        , color = "rgb(0,0,0)"
        }
    }


backFace : FaceConfig
backFace =
    { id = "back-face"
    , groupName = "backFaceAnim"
    , label = "BACK"
    , background = "rgb(41, 128, 185)"
    , borderColor = "rgb(33, 97, 140)"
    , text =
        { id = "back-face-text"
        , groupName = "backFaceTextAnim"
        , label = "BACK"
        , color = "rgb(0,0,0)"
        }
    }


rightFace : FaceConfig
rightFace =
    { id = "right-face"
    , groupName = "rightFaceAnim"
    , label = "RIGHT"
    , background = "rgb(231, 76, 60)"
    , borderColor = "rgb(192, 57, 43)"
    , text =
        { id = "right-face-text"
        , groupName = "rightFaceTextAnim"
        , label = "RIGHT"
        , color = "rgb(0,0,0)"
        }
    }


leftFace : FaceConfig
leftFace =
    { id = "left-face"
    , groupName = "leftFaceAnim"
    , label = "LEFT"
    , background = "rgb(230, 126, 34)"
    , borderColor = "rgb(211, 84, 0)"
    , text =
        { id = "left-face-text"
        , groupName = "leftFaceTextAnim"
        , label = "LEFT"
        , color = "rgb(0,0,0)"
        }
    }


topFace : FaceConfig
topFace =
    { id = "top-face"
    , groupName = "topFaceAnim"
    , label = "TOP"
    , background = "rgb(46, 204, 113)"
    , borderColor = "rgb(39, 174, 96)"
    , text =
        { id = "top-face-text"
        , groupName = "topFaceTextAnim"
        , label = "TOP"
        , color = "rgb(0,0,0)"
        }
    }


bottomFace : FaceConfig
bottomFace =
    { id = "bottom-face"
    , groupName = "bottomFaceAnim"
    , label = "BOTTOM"
    , background = "rgb(155, 89, 182)"
    , borderColor = "rgb(142, 68, 173)"
    , text =
        { id = "bottom-face-text"
        , groupName = "bottomFaceTextAnim"
        , label = "BOTTOM"
        , color = "rgb(0,0,0)"
        }
    }



-- ANIMATIONS
--
-- CUBE
--
-- We only rotate the cube, not individual faces, they maintain their
-- position in 3D space because we use `View3D.transformStyle View3D.Preserve3D`
-- on the cube container


rotateCube : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCube to =
    WAAPI.for cubeGroupName
        >> Rotate.begin
        >> Rotate.toXYZ to to to
        >> Rotate.easing BackInOut
        >> Rotate.duration 8000
        >> Rotate.end


rotateCubeClockwise : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCubeClockwise =
    rotateCube 360


rotateCubeAntiClockwise : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCubeAntiClockwise =
    rotateCube 0



-- SIDES


moveSidesOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveSidesOut targetAmount =
    moveFrontFaceOut targetAmount
        >> moveBackFaceOut targetAmount
        >> moveRightFaceOut targetAmount
        >> moveLeftFaceOut targetAmount
        >> moveTopFaceOut targetAmount
        >> moveBottomFaceOut targetAmount


moveSidesIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveSidesIn targetAmount =
    moveFrontFaceIn targetAmount
        >> moveBackFaceIn targetAmount
        >> moveRightFaceIn targetAmount
        >> moveLeftFaceIn targetAmount
        >> moveTopFaceIn targetAmount
        >> moveBottomFaceIn targetAmount


sharedTiming : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
sharedTiming =
    WAAPI.duration 1000
        >> WAAPI.easing CircInOut


moveFace : FaceConfig -> (Translate.Builder ForWAAPI -> Translate.Builder ForWAAPI) -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFace config moveToBuilder =
    sharedTiming
        >> WAAPI.for config.groupName
        >> Translate.begin
        >> moveToBuilder
        >> Translate.end



-- Each face moves along the axis it faces by a `moveAmount` (expressed in
-- `Cqmin`, so it scales with the stage) when the cube expands, and moves
-- back to its original position when the cube closes.
--
-- Front/Back faces move on Z (forward/backward)
-- Left/Right faces move on X (sideways)
-- Top/Bottom faces move on Y (up/down)


moveAmount : Float
moveAmount =
    10


moveFrontFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFrontFaceOut toZ =
    moveFace frontFace <|
        Translate.toZ (toZ + moveAmount)


moveFrontFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFrontFaceIn toZ =
    moveFace frontFace <|
        Translate.toZ toZ


moveBackFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBackFaceOut toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ - moveAmount)


moveBackFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBackFaceIn toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ)


moveRightFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveRightFaceOut toX =
    moveFace rightFace <|
        Translate.toX (toX + moveAmount)


moveRightFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveRightFaceIn toX =
    moveFace rightFace <|
        Translate.toX toX


moveLeftFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveLeftFaceOut toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX - moveAmount)


moveLeftFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveLeftFaceIn toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX)


moveTopFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTopFaceOut toY =
    moveFace topFace <|
        Translate.toY (-1 * toY - moveAmount)


moveTopFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTopFaceIn toY =
    moveFace topFace <|
        Translate.toY (-1 * toY)


moveBottomFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBottomFaceOut toY =
    moveFace bottomFace <|
        Translate.toY (toY + moveAmount)


moveBottomFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBottomFaceIn toY =
    moveFace bottomFace <|
        Translate.toY toY



-- TEXT
--
-- Text moves forward (Z+4cqmin) and rotates (to Z=360deg) when sides expand,
-- and then moves back (to Z=0cqmin) and rotates back (to Z=0deg) when sides close


textMoveAmount : Float
textMoveAmount =
    14


moveText : TextConfig -> Float -> Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveText config toZ toRotate =
    sharedTiming
        >> WAAPI.for config.groupName
        >> Translate.begin
        >> Translate.toZ toZ
        >> Translate.end
        >> Rotate.begin
        >> Rotate.toZ toRotate
        >> Rotate.end


moveTextsOut : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTextsOut =
    moveText frontFace.text textMoveAmount 360
        >> moveText backFace.text textMoveAmount 360
        >> moveText rightFace.text textMoveAmount 360
        >> moveText leftFace.text textMoveAmount 360
        >> moveText topFace.text textMoveAmount 360
        >> moveText bottomFace.text textMoveAmount 360


moveTextsIn : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTextsIn =
    moveText frontFace.text 0 0
        >> moveText backFace.text 0 0
        >> moveText rightFace.text 0 0
        >> moveText leftFace.text 0 0
        >> moveText topFace.text 0 0
        >> moveText bottomFace.text 0 0





selectAnimation : Float -> State -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
selectAnimation targetAmount state =
    case state of
        Opening ->
            moveSidesOut targetAmount
                >> moveTextsOut

        Closing ->
            moveSidesIn targetAmount
                >> moveTextsIn

        RotatingOpen ->
            rotateCubeClockwise

        RotatingClosed ->
            rotateCubeAntiClockwise



-- UPDATE


type Msg
    = NoOp
    | GotWaapiMsg WAAPI.AnimMsg
    | TriggerAnimation





update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            ( model, Cmd.none )

        TriggerAnimation ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        selectAnimation depth model.state
            in
            ( { model | animState = animState }
            , cmd
            )

        GotWaapiMsg animMsg ->
            let
                ( animState, maybeAnimEvent ) =
                    WAAPI.update animMsg model.animState
            in
            case maybeAnimEvent of
                Just animEvent ->
                    handleMotionEvent animEvent { model | animState = animState }

                Nothing ->
                    ( { model | animState = animState }, Cmd.none )


handleMotionEvent : WAAPI.AnimEvent -> Model -> ( Model, Cmd Msg )
handleMotionEvent animEvent model =
    case animEvent of
        WAAPI.Ended "cubeAnim" ->
            cubeRotationEnded model

        WAAPI.Ended "frontFaceAnim" ->
            sidesMovementEnded model

        _ ->
            ( model, Cmd.none )


cubeRotationEnded : Model -> ( Model, Cmd Msg )
cubeRotationEnded model =
    case model.state of
        RotatingOpen ->
            stateChanged Closing model

        RotatingClosed ->
            stateChanged Opening model

        _ ->
            ( model, Cmd.none )


sidesMovementEnded : Model -> ( Model, Cmd Msg )
sidesMovementEnded model =
    case model.state of
        Opening ->
            stateChanged RotatingOpen model

        Closing ->
            stateChanged RotatingClosed model

        _ ->
            ( model, Cmd.none )


stateChanged : State -> Model -> ( Model, Cmd Msg )
stateChanged state model =
    let
        ( animState, cmd ) =
            WAAPI.animate model.animState <|
                selectAnimation depth state
    in
    ( { model
        | state = state
        , animState = animState
      }
    , cmd
    )



-- SUBSCRIPTIONS


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



-- VIEW


view : Model -> Document Msg
view model =
    { title = "WAAPI 3D Example"
    , body =
        [ div
            [ Html.Attributes.class "example-stage"
            , id "example-stage"
            , style "width" "min(90vw, 90vh)"
            , style "height" "min(90vw, 90vh)"
            , style "container-type" "size"
            ]
            [ viewAnimationArea model
            ]
        ]
    }


viewAnimationArea : Model -> Html Msg
viewAnimationArea model =
    div
        [ -- Perspective container
          View3D.perspective 1000
        , View3D.perspectiveOrigin View3D.Center

        --
        -- Hack for Chrome on macOS GPU compositing issues with 3D transforms.
        -- Setting opacity: 0.99 forces a new compositing layer, which prevents
        -- the colored rectangle artifacts that can appear during complex 3D animations.
        -- It's not perfect, some flickering can still occur.
        , View3D.opacityHack
        , id "animation-area"
        , style "display" "flex"
        , style "justify-content" "center"
        , style "align-items" "center"
        , style "width" "100%"
        , style "height" "100%"
        , style "flex" "0 0 auto"
        ]
        [ viewCube model ]





viewCube : Model -> Html Msg
viewCube model =
    div
        (WAAPI.attributes cubeGroupName model.animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id "cube"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "position" "relative"
               ]
        )
        [ viewFace model.animState frontFace
        , viewFace model.animState backFace
        , viewFace model.animState rightFace
        , viewFace model.animState leftFace
        , viewFace model.animState topFace
        , viewFace model.animState bottomFace
        ]


viewFace : WAAPI.AnimState Msg -> FaceConfig -> Html Msg
viewFace animState config =
    div
        (WAAPI.attributes config.groupName animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id config.id
               , style "position" "absolute"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "background-color" config.background
               , style "border" ("2px solid " ++ config.borderColor)
               , style "box-sizing" "border-box"
               , style "display" "flex"
               , style "justify-content" "center"
               , style "align-items" "center"
               , style "font-weight" "bold"
               , style "font-size" ("calc(" ++ cubeSizeCss ++ " * 0.13)")
               ]
        )
        [ div
            [ style "color" "#ffffff"
            , style "position" "absolute"
            ]
            [ text config.label ]
        , div
            (WAAPI.attributes config.text.groupName animState
                ++ [ id config.text.id
                   , style "color" config.text.color
                   , style "position" "absolute"
                   ]
            )
            [ text config.text.label ]
        ]

Quick Walkthrough

Here's a general workflow to get up an running quickly.

1. Build

View Source Code
import Anim.Engine.WAAPI as WAAPI
import Anim.Property.Opacity as Opacity


fadeIn : WAAPI.AnimBuilder eng -> WAAPI.AnimBuilder eng
fadeIn =
    Opacity.begin
        >> Opacity.to 1
        >> Opacity.end

2. Initialize

Make your module a port module, define your ports: motionCmd and motionMsg, and pass them to init with your property initializers.

View Source Code
port module Main exposing (main)

import Json.Decode
import Json.Encode

-- Outgoing to JS
port motionCmd : Json.Encode.Value -> Cmd msg

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


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


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

3. Render

Render WAAPI attributes on the animated element.

View Source Code
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick TriggerFadeIn ] [ text "Fade In" ]
        , div (WAAPI.attributes "card" model.animState) [ text "Animated card" ]
        ]

4. Trigger with animate

Call animate to start a state-tracked animation.

View Source Code
TriggerFadeIn ->
    let
        ( animState, cmd ) =
            WAAPI.animate model.animState <|
                WAAPI.for "card"
                    >> fadeIn
    in
    ( { model | animState = animState }, cmd )

5. React

Subscribe to events, then process messages with update.

View Source Code
type Msg
    = TriggerFadeIn
    | GotAnimMsg WAAPI.AnimMsg

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

        _ ->
            ( model, Cmd.none )

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

In Detail

Initialize

The WAAPI engine communicates through two ports: one outgoing (Elm → JS) and one incoming (JS → Elm). Define them in your port module, then pass them to init along with property initializers.

View Source Code
port module Main exposing (main)

import Json.Encode

-- Outgoing port (Elm → JS): sends all animation commands
port motionCmd : Json.Encode.Value -> Cmd msg

-- Incoming port (JS → Elm): receives all animation messages
port motionMsg : (Json.Encode.Value -> msg) -> Sub msg

init : ( Model, Cmd Msg )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg
                [ Opacity.init "fadeAnim" 0
                , Translate.initXY "slideAnim" 100 50
                ]
      }
    , Cmd.none
    )

📖 See Initialize for more info.

📖 See WAAPI JavaScript for install instructions.

Trigger

The WAAPI engine offers two trigger functions: animate for state-tracked animations and fireAndForget for fire-and-forget effects.

animate

Use animate when you need state-tracked animations. The engine tracks start values, so subsequent animations always start from the last known position.

View Source Code
TriggerFadeIn ->
    let
        ( animState, cmd ) =
            WAAPI.animate model.animState fadeIn
    in
    ( { model | animState = animState }, cmd )

fireAndForget

Use fireAndForget for one-shot effects where you don't need to pause, resume, query, or interrupt. It takes the port function directly and returns a bare Cmd msg with no state to store.

View Source Code
TriggerFadeIn ->
    ( model
    , WAAPI.fireAndForget motionCmd fadeIn
    )

📖 See Triggering Animations for more info.

Mid-Flight Interruptions

WAAPI keeps runtime animation state, so interrupting a running animation continues smoothly from the current in-flight position.

📖 See Interrupting Animations for more info.

OnLoad Animations

For on-load animations, trigger animate when the page initializes, the animation runs immediately.

Update

Use update to process incoming WAAPI messages. It returns the updated AnimState and a Maybe AnimEvent.

View Source Code
GotAnimMsg animMsg ->
    let
        ( animState, maybeEvent ) =
            WAAPI.update animMsg model.animState
    in
    handleEvent maybeEvent { model | animState = animState }

Events

The WAAPI, ScrollTimeline and ViewTimeline Engines all utilize the JavaScript Web Animations API, and they all use the same ports to communicate with the JS companion. If you use two or more of these engines in your Elm App, depending on your setup, there is the potential for them all to receive the same messages from JS at the same time, which could be confusing.

The library has you covered here though, all incoming messages are gated by each Engine, which is why update returns a Maybe AnimEvent - Nothing means the message was not for this Engine.

Every event carries the animation group name. Some events carry an additional value:

  • Cancelled and Paused include the progress at the moment of cancellation/pause (Float, 0.0–1.0)
  • Iteration includes the iteration count (Int)
  • Progress fires every frame with the current progress (Float, 0.0–1.0)
  • AnimError carries an error string from the JavaScript layer
View Source Code
handleEvent : Maybe AnimEvent -> Model -> ( Model, Cmd Msg )
handleEvent maybeEvent model =
    case maybeEvent of
        Just (Started "box") ->
            ( model, Cmd.none )

        Just (Ended "box") ->
            ( model, Cmd.none )

        Just (AnimError err) ->
            ( model, Cmd.none )

        _ ->
            ( model, Cmd.none )
Event Fires when...
Started Animation begins playing
Ended Animation completes
Cancelled Animation is interrupted by something outside the engine's control.
Iteration Each iteration completes (looping or alternating)
Progress Every frame while the animation is running
Paused pause is called on a running animation
Resumed resume is called on a paused animation
Restarted restart is called
AnimError The JavaScript layer reports an error

Subscriptions

The WAAPI engine requires a subscription to receive animation events from JavaScript. Without it, animations still play visually but Elm won't receive events and AnimState will be out of sync.

View Source Code
subscriptions : Model -> Sub Msg
subscriptions model =
    WAAPI.subscriptions GotAnimMsg model.animState

📖 See React for more info.

View

Apply attributes to the animated element.

View Source Code
div (WAAPI.attributes "card" model.animState) [ text "Animated card" ]

📖 See Render for more info.

Responsive Strategy

Use relative CSS units whenever the motion can be defined in layout-relative terms.

For measured pixel targets, WAAPI supports proportional remap for resize updates.

  • On resize, update bounds with onResize and Translate.bounds / Scale.bounds / PerspectiveOrigin.bounds.
  • Running animations remap to the equivalent relative position inside the updated bounds.
  • Idle animations also re-position proportionally inside the updated bounds.

📖 See Responsive Animations for more info.

Playback

Set iterations, loopForever, and alternate in the animation builder.

View Source Code
spinForever =
    WAAPI.loopForever
        >> WAAPI.alternate
        >> WAAPI.for "icon"
        >> Rotate.begin
        >> Rotate.toZ 360
        >> Rotate.duration 1000
        >> Rotate.end

📖 See Playback for the full looping, iterations, and alternate API with live examples.

Timing

Set the default duration, speed, and delay. Inherited by every property that doesn't override them.

  • duration — animation length in milliseconds.
  • speed — alternative to duration; set a rate in property units per second.
  • delay — wait before the animation begins, in milliseconds.
View Source Code
fadeIn =
    WAAPI.delay 500
        >> WAAPI.duration 800
        >> WAAPI.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.end

📖 See Timing for more info.

Easing

WAAPI animations support the full Easing library, including bounce and elastic. Simple curves (sine, quad, cubic, quart, quint, expo) are handed to the browser as native easing functions. Complex curves (bounce, elastic) are sampled into densely-spaced stops, and the browser interpolates linearly between them — visually faithful to the source curve.

Set the default easing for all properties that don't override it:

View Source Code
fadeIn =
    WAAPI.easing CubicInOut
        >> WAAPI.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 300
        >> Opacity.delay 50
        >> Opacity.end

📖 See Easing for all available easing functions.

Spring

WAAPI animations support springs. The spring's motion is pre-baked into densely-spaced keyframe stops on the Elm side, and the browser interpolates linearly between them — visually faithful to the analytic solution.

Set the default spring for all properties that don't override it: The motion ends when each value has settled at the target — there is no explicit duration.

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

📖 See Spring for the full preset list and tuning guidance.

Controls

All WAAPI control functions return ( AnimState msg, Cmd msg ) — the Cmd is dispatched to JavaScript to drive the underlying Web Animation.

Function Description
stop Jump to end state
reset Jump to start state
restart Reset and begin playing again
pause Freeze at current position
resume Continue from paused position

📖 See Controlling Animations for more info.

Discrete Properties

The WAAPI engine manages discrete properties as inline styles. discreteEntry values are applied immediately when the animation starts, and discreteExit values flip when the animation completes. No additional view setup is needed.

📖 See Discrete Properties for the full API, live examples, and source code.

Transform Order

Use transformOrder to set the order in which transform properties are applied.

Call it after for to set the current animation group's order. Call it before selecting a group to set the global default for groups that do not override it.

View Source Code
import Anim.Extra.TransformOrder exposing (TransformProperty(..))

animateBox =
    WAAPI.for "box"
        >> WAAPI.transformOrder [ Scale, Rotate, Translate ]
        >> Translate.begin
        >> ...

📖 See Transform Order for full details.

Freeze Axes

Freeze individual axes of transform properties so they remain fixed during an animation. This is useful when animating one axis while holding another in place.

FreezeProperty values: translate, rotate, scale, skew.

View Source Code
-- Animate translate X, freeze Y so the element only moves horizontally
slideRight : WAAPI.AnimBuilder eng -> WAAPI.AnimBuilder eng
slideRight =
    WAAPI.freezeY [ WAAPI.translate ]
        >> WAAPI.for "box"
        >> Translate.begin
        >> Translate.toX 200
        >> Translate.duration 400
        >> Translate.end

Call unfreezeY (or the matching unfreeze* variant) in a subsequent animation to release the frozen axis.

📖 See Freezing Axes with freeze* for more info.

State Queries

Query animation state.

View Source Code
WAAPI.anyRunning model.animState           -- Maybe Bool
WAAPI.isRunning "box" model.animState      -- Maybe Bool
WAAPI.allComplete model.animState          -- Maybe Bool
WAAPI.isComplete "box" model.animState     -- Maybe Bool
WAAPI.isCancelled "box" model.animState    -- Maybe Bool
WAAPI.getProgress "box" model.animState    -- Maybe Float (0.0–1.0)

Nothing is returned when no animation exists for the given group.

Property Queries

Query the current, start, and end values for any animated property.

View Source Code
WAAPI.getOpacityStart "box" model.animState    -- Maybe Float
WAAPI.getOpacityEnd "box" model.animState      -- Maybe Float
WAAPI.getOpacityCurrent "box" model.animState  -- Maybe Float
WAAPI.getTranslateCurrent "box" model.animState -- Maybe { x, y, z }

Nothing is returned when no animation exists for the given group.

When to Choose This Engine

Choose WAAPI when you want browser-native playback with the broadest state-tracked feature set.

  • Best for: production animations that need strong control, events, and current-value queries.
  • Avoid when: you do not want JavaScript ports or companion setup.

API Quick Reference

Types

Type Description
AnimState msg Tracks animations and their states
AnimBuilder eng Carries all animation configurations
AnimMsg Messages from WAAPI subscription
AnimEvent Events returned by update
AnimGroupName String type alias for the animation group name
TransformProperty Custom transform ordering
FreezeProperty Identifies a transform axis to freeze

Initialize

Function Type Description
init (Value -> Cmd msg) -> ((Value -> msg) -> Sub msg) -> List (AnimBuilder eng -> AnimBuilder eng) -> AnimState msg Create initial animation state with ports

Trigger

Function Type Description
animate AnimState msg -> (AnimBuilder eng -> AnimBuilder eng) -> ( AnimState msg, Cmd msg ) Apply a state-tracked animation
fireAndForget (Value -> Cmd msg) -> (AnimBuilder eng -> AnimBuilder eng) -> Cmd msg Fire a stateless animation

Events

Event Description
Started AnimGroupName Animation begins playing
Ended AnimGroupName Animation completes
Cancelled AnimGroupName Float Animation cancelled; Float is progress at cancellation
Restarted AnimGroupName Animation is restarted
Paused AnimGroupName Float Animation paused; Float is progress at pause
Resumed AnimGroupName Animation resumed
Iteration AnimGroupName Int Loop iteration completes; Int is iteration count
Progress AnimGroupName Float Each frame; Float is current progress (0.0–1.0)
AnimError String JavaScript-layer error

Update

Function Type Description
update AnimMsg -> AnimState msg -> ( AnimState msg, Maybe AnimEvent ) Process WAAPI messages

Subscriptions

Function Type Description
subscriptions (AnimMsg -> msg) -> AnimState msg -> Sub msg Subscribe to WAAPI events from JavaScript

View

Function Type Description
attributes AnimGroupName -> AnimState msg -> List (Html.Attribute msg) Get animation attributes for an element

Playback

Function Type Description
iterations Int -> AnimBuilder eng -> AnimBuilder eng Set number of iterations
loopForever AnimBuilder eng -> AnimBuilder eng Loop animation infinitely
alternate AnimBuilder eng -> AnimBuilder eng Reverse direction on each iteration

Timing

Function Type Description
duration Int -> AnimBuilder eng -> AnimBuilder eng Set duration (ms)
speed Float -> AnimBuilder eng -> AnimBuilder eng Set speed (property units/sec)
delay Int -> AnimBuilder eng -> AnimBuilder eng Set delay before animation starts (ms)

Easing

Function Type Description
easing Easing -> AnimBuilder eng -> AnimBuilder eng Set easing function

Spring

Function Type Description
spring Spring -> AnimBuilder eng -> AnimBuilder eng Set spring physics

Controls

Function Type Description
pause AnimGroupName -> AnimState msg -> ( AnimState msg, Cmd msg ) Freeze at current position
resume AnimGroupName -> AnimState msg -> ( AnimState msg, Cmd msg ) Continue from paused position
stop AnimGroupName -> AnimState msg -> ( AnimState msg, Cmd msg ) Jump to end state and stop
reset AnimGroupName -> AnimState msg -> ( AnimState msg, Cmd msg ) Jump to start state and stop
restart AnimGroupName -> AnimState msg -> ( AnimState msg, Cmd msg ) Reset and begin playing again

Discrete Properties

Function Type Description
discreteEntry String -> String -> AnimBuilder eng -> AnimBuilder eng Set a CSS property value when the animation starts
discreteExit String -> String -> String -> AnimBuilder eng -> AnimBuilder eng Set a CSS property value during and after the animation

Transform Order

Function Type Description
transformOrder List TransformProperty -> AnimBuilder eng -> AnimBuilder eng Set custom transform order

Freeze Axes

Function Type Description
translate FreezeProperty Target translate for freezing
rotate FreezeProperty Target rotate for freezing
scale FreezeProperty Target scale for freezing
skew FreezeProperty Target skew for freezing
freezeX List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze X axis of specified properties
freezeY List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze Y axis
freezeZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze Z axis
freezeXY List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze X and Y axes
freezeXZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze X and Z axes
freezeYZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze Y and Z axes
freezeXYZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Freeze all axes
unfreezeX List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze X axis
unfreezeY List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze Y axis
unfreezeZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze Z axis
unfreezeXY List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze X and Y axes
unfreezeXZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze X and Z axes
unfreezeYZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze Y and Z axes
unfreezeXYZ List FreezeProperty -> AnimBuilder eng -> AnimBuilder eng Unfreeze all axes

State Queries

Function Type Description
anyRunning AnimState msg -> Maybe Bool Check if any animation is running
isRunning AnimGroupName -> AnimState msg -> Maybe Bool Check if a specific group is animating
allComplete AnimState msg -> Maybe Bool Check if all animations are complete
isComplete AnimGroupName -> AnimState msg -> Maybe Bool Check if a specific group's animation is complete
isCancelled AnimGroupName -> AnimState msg -> Maybe Bool Check if a specific group's animation was cancelled
getProgress AnimGroupName -> AnimState msg -> Maybe Float Get current progress (0.0–1.0)

Property Queries

Function Type Description
getOpacityStart AnimGroupName -> AnimState msg -> Maybe Float Get start opacity
getOpacityEnd AnimGroupName -> AnimState msg -> Maybe Float Get end opacity
getOpacityCurrent AnimGroupName -> AnimState msg -> Maybe Float Get current opacity
getTranslateStart AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get start translate
getTranslateEnd AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get end translate
getTranslateCurrent AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get current translate
getRotateStart AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get start rotate
getRotateEnd AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get end rotate
getRotateCurrent AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get current rotate
getScaleStart AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get start scale
getScaleEnd AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get end scale
getScaleCurrent AnimGroupName -> AnimState msg -> Maybe { x, y, z } Get current scale
getSizeStart AnimGroupName -> AnimState msg -> Maybe { width, height } Get start size
getSizeEnd AnimGroupName -> AnimState msg -> Maybe { width, height } Get end size
getSizeCurrent AnimGroupName -> AnimState msg -> Maybe { width, height } Get current size
getSkewStart AnimGroupName -> AnimState msg -> Maybe { x, y } Get start skew
getSkewEnd AnimGroupName -> AnimState msg -> Maybe { x, y } Get end skew
getSkewCurrent AnimGroupName -> AnimState msg -> Maybe { x, y } Get current skew
getPropertyStart AnimGroupName -> String -> AnimState msg -> Maybe Float Get start value for a custom numeric property
getPropertyEnd AnimGroupName -> String -> AnimState msg -> Maybe Float Get end value for a custom numeric property
getPropertyCurrent AnimGroupName -> String -> AnimState msg -> Maybe Float Get current value for a custom numeric property
getColorPropertyStart AnimGroupName -> String -> AnimState msg -> Maybe Color Get start value for a custom color property
getColorPropertyEnd AnimGroupName -> String -> AnimState msg -> Maybe Color Get end value for a custom color property
getColorPropertyCurrent AnimGroupName -> String -> AnimState msg -> Maybe Color Get current value for a custom color property

Nothing is returned when no animation exists for the given group.

For complete API details, see the Anim.Engine.WAAPI documentation.

Next Steps

Explore the related timeline engines:

Scroll Timeline Engine Or View Timeline Engine

Or review migration paths and tradeoffs.

Migration Guide →