I remembered using a project called Animate.css in the bad old JS days, and wanted to see how easy it would be to use in an Elm application.

Animate.css provides a set of cross-browser CSS animations using opacity and transform . They work across browsers and have good performance. From an Elm perspective, it’s also convenient not to have to track fine-grained animation state in the model and let the browser do it instead.

It’s also cool that Animate.css takes accessibility into account by supporting the prefers-reduced-motion media query. It’s well supported and allows users to disable CSS transitions by setting an option for reduced motion in OS settings.

General usage

To use Animate.css, we just need to include its CSS file in the page:

<head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css"> </head>

An element can be animated by adding a few classes:

<h1 class="animated infinite pulse faster delay-2s">Animated header</h1>

There are quite a number of animations included: fading, zooming, rotations, slides and so on.

The infinite class makes the animation loop indefinitely. Without it, it only plays once.

It’s also possible to delay the start of an animation by adding a class such as delay-1s or delay-2s . Animations can be sped up or slowed down with the help of slow , slower , fast and faster classes.

An animated button

Let’s make a button that starts pulsing on hover:

The Elm code looks like this:

module Main exposing (..) import Browser import Html exposing (..) import Html.Attributes as Attr import Html.Events exposing (..) type AnimationState = None | Pulse type Msg = UserHoveredButton | UserUnhoveredButton type alias Model = { animationState : AnimationState } update : Msg -> Model -> Model update msg model = case msg of UserHoveredButton -> { model | animationState = Pulse } UserUnhoveredButton -> { model | animationState = None } view : Model -> Html Msg view model = [ button [ Attr.style "width" "150px" , Attr.style "border" "3px solid #000" , Attr.style "border-radius" "6px" , Attr.style "background-color" "#fff" , Attr.style "font-size" "32px" , Attr.classList [ ( "animated", model.animationState == Pulse ) , ( "pulse", model.animationState == Pulse ) , ( "infinite", model.animationState == Pulse ) ] , onMouseEnter <| UserHoveredButton , onMouseLeave <| UserUnhoveredButton ] [ text "Button" ] ] |> div [ Attr.style "width" "150px" , Attr.style "margin" "100px auto 0 auto" , Attr.style "text-align" "center" ] main : Program () Model Msg main = Browser.sandbox { init = { animationState = None } , view = view , update = update }

In the view function, I’m conditionally adding the animation classes depending on the state of the button. The animation state is changed via two event handlers: onMouseEnter and onMouseLeave .

More buttons!

What if I want to have three animated buttons instead of one? It would look like this:

Then I have to keep track of their state in the model, and I need to know which button is producing the messages. To keep things simple, we can go with a Dict of animation states keyed by a string ID in the model, and send the ID of the active button along with the UserHoveredButton and UserUnhoveredButton messages.

Here is the code with the relevant changes:

module Main exposing (..) import Browser import Dict exposing (Dict) import Html exposing (..) import Html.Attributes as Attr import Html.Events exposing (..) type AnimationState = None | Pulse type Msg = UserHoveredButton String | UserUnhoveredButton String type alias Model = { animationState : Dict String AnimationState } update : Msg -> Model -> Model update msg model = case msg of UserHoveredButton id -> { model | animationState = Dict.insert id Pulse model.animationState } UserUnhoveredButton id -> { model | animationState = Dict.insert id None model.animationState } view : Model -> Html Msg view model = List.range 1 3 |> List.map (\i -> let animState = Dict.get (String.fromInt i) model.animationState |> Maybe.withDefault None in div [] [ button [ Attr.style "width" "150px" , Attr.style "border" "3px solid #000" , Attr.style "border-radius" "6px" , Attr.style "background-color" "#fff" , Attr.style "font-size" "32px" , Attr.classList [ ( "animated", animState /= None ) , ( "pulse", animState /= None ) , ( "infinite", animState /= None ) ] , onMouseEnter <| UserHoveredButton <| String.fromInt i , onMouseLeave <| UserUnhoveredButton <| String.fromInt i ] [ text "Button" ] ] ) |> div [ Attr.style "width" "150px" , Attr.style "margin" "100px auto 0 auto" , Attr.style "text-align" "center" ] main : Program () Model Msg main = Browser.sandbox { init = { animationState = Dict.fromList [ ( "1", None ), ( "2", None ), ( "3", None ) ] } , view = view , update = update }

Still quite straightforward!

More animation!

How about fancier animations? Let’s say I want each button to rotate first and then pulse on hover:

On hover, each button will do a single rotateIn animation and then switch to an infinite pulse animation.

To do that, I need to know when the first part of the animation finishes. At that point I can change the class from rotateIn to pulse . Luckily, that’s possible to do by handling the animationend event.

The types need to change to support this extra snazzy UI:

type AnimationState = None | Rotate | Pulse type Msg = BrowserFinishedAnimation String | UserHoveredButton String | UserUnhoveredButton String

Each button will now cycle through three animation states, and there is a new message corresponding to the animationend event.

The update function becomes a bit more involved:

update : Msg -> Model -> Model update msg model = case msg of BrowserFinishedAnimation id -> let nextState = Maybe.map (\s -> if s == Rotate then Pulse else s) in { model | animationState = Dict.update id nextState model.animationState } UserHoveredButton id -> let nextState = Maybe.map (\s -> if s == None then Rotate else s) in { model | animationState = Dict.update id nextState model.animationState } UserUnhoveredButton id -> { model | animationState = Dict.insert id None model.animationState }

Lastly, the view function gets more elaborate when building the class list, and each button acquires an on "animationend" attribute:

view : Model -> Html Msg view model = List.range 1 3 |> List.map (\i -> let animState = Dict.get (String.fromInt i) model.animationState |> Maybe.withDefault None in div [ onMouseEnter <| UserHoveredButton <| String.fromInt i , onMouseLeave <| UserUnhoveredButton <| String.fromInt i ] [ button [ Attr.style "width" "150px" , Attr.style "border" "3px solid #000" , Attr.style "border-radius" "6px" , Attr.style "background-color" "#fff" , Attr.style "font-size" "32px" , Attr.classList [ ( "animated", animState /= None ) , ( if animState == Rotate then "rotateIn" else "pulse", animState /= None ) , ( "infinite", animState == Pulse ) ] , on "animationend" <| Decode.succeed <| BrowserFinishedAnimation <| String.fromInt i ] [ text "Button" ] ] ) |> div [ Attr.style "width" "150px" , Attr.style "margin" "100px auto 0 auto" , Attr.style "text-align" "center" ]

You may notice one more change: each button is wrapped in a div , and the hover/unhover messages are now triggered from the div rather than the button itself.

The reason for this is that the rotation animation changes the boundaries of the element, which in turn triggers a chain reaction of onMouseLeave / onMouseEnter events and multiple restarts of the animation. Moving the events to a parent element solves the problem.

Other options

There are other options for implementing animation. You can roll your own CSS. You can roll your own animations in Elm using the onAnimationFrame event.

(I was amused to see that searching for “animation” on the package site brings up a link to onAnimationFrame as well as a list of animation-related packages.)

Alternatively, you can also use an Elm package like mdgriffith/elm-style-animation or z5h/timeline for pure Elm animation.

These approaches have different tradeoffs.

Conclusion

My experiment shows that Animate.css can be combined with Elm, and is an easy way to add simple prepackaged CSS animations to the UI.

Interesting effects can be achieved by combining multiple animations.