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
)
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
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.
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 : 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
, 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.
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 ->
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
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
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.