Skip to content

CSS Keyframe Engine

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

This Engine builds native browser CSS @keyframes animations. The browser handles all rendering, providing excellent performance.

Example

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

View Example

View Source Code
module Animation.Keyframe.PulsingDot.Main exposing (main)

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



-- 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
                [ Scale.init groupName 1
                , Opacity.init groupName 1
                ]
    in
    ( { animState =
            Keyframe.animate animState <|
                Keyframe.for groupName
                    >> pulse
      }
    , Cmd.none
    )



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


groupName : String
groupName =
    "pulsingDot"


pulse : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
pulse =
    Keyframe.loopForever
        >> Keyframe.alternate
        >> Keyframe.duration 1000
        >> Keyframe.easing EaseInOut
        >> Scale.begin
        >> Scale.to 0.4
        >> Scale.end
        >> Opacity.begin
        >> Opacity.to 0.3
        >> Opacity.end



-- VIEW


view : Model -> Html msg
view model =
    div
        [ class "example-stage" ]
        [ Keyframe.styleNode model.animState
        , div
            (Keyframe.attributes groupName model.animState
                ++ [ style "width" "80px"
                   , style "height" "80px"
                   , style "border-radius" "50%"
                   , style "background-color" "#e53935"
                   ]
            )
            []
        ]

Quick Walkthrough

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

1. Build

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


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

2. Initialize

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


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

3. Render

Render the generated @keyframes style node, element attributes and event listeners.

View Source Code
view : Model -> Html Msg
view model =
    div []
        [ Keyframe.styleNodeFor "card" model.animState
        , div 
            (Keyframe.attributes "card" model.animState
                ++ Keyframe.events GotAnimMsg
            )
            [ text "Animated card" ]
        ]

4. Trigger with animate

Call animate to apply the animation config to the current AnimState.

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

5. React

Use update for incoming Keyframe events.

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


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

        _ ->
            ( model, Cmd.none )

handleAnimEvent : Maybe AnimEvent -> Model -> (Model, Cmd Msg)
handleAnimEvent maybeEvent model =
    case maybeEvent of 
        Just (Ended _ _ "card") ->
            (model, Cmd.none)

        _ ->
            (model, Cmd.none)

In Detail

Initialize

Pass a list of property initializers to init. Each registers an animation group name and sets the element's starting inline style from the first render.

View Source Code
init : ( Model, Cmd Msg )
init =
    ( { animState = Keyframe.init [ Opacity.init "card" 0 ] }
    , Cmd.none
    )

📖 See Initialize for more info.

Trigger

Call animate to apply an animation to the current AnimState.

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

📖 See Triggering Animations for more info.

Mid-Flight Interruptions

Mid-flight values are not available for @keyframes animations, which means triggering with animate while an animation is running cancels the current animation and replaces it with the new one. This will cause a jump in state from wherever the animation was, to the start state of the new animation.

📖 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 keyframe messages. It returns the updated AnimState and the corresponding AnimEvent.

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

Events

update returns a single AnimEvent per call.

DOM events (Started, Ended, Cancelled, Iteration) carry three values:

  • the id (if one exists) of the element that fired the event (CurrentTargetId),
  • the id (if one exists) of the element that owns the listener (TargetId), and,
  • the animation group name.

Iteration carries an additional iteration count (Int).

In most cases only the group name is needed. CurrentTargetId and TargetId may or may not be the same depending on whether the event has bubbled up.

Synthetic events (Paused, Resumed, Restarted) are generated by the engine when the corresponding control functions are called, and carry only the animation group name.

View Source Code
handleAnimEvent : Maybe Keyframe.AnimEvent -> Model -> ( Model, Cmd Msg )
handleAnimEvent event model =
    case event of
        Just (Keyframe.Ended _ _ "card") ->
            ( 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)
Paused pause is called on a running animation
Resumed resume is called on a paused animation
Restarted restart is called

📖 See React for more info.

View

Apply attributes to the animated element and include styleNode in your view to inject the generated @keyframes CSS rules.

View Source Code
view : Model -> Html Msg
view model =
    div []
        [ Keyframe.styleNode model.animState
        , div (Keyframe.attributes "card" model.animState) [ text "Animated card" ]
        ]

Use styleNodeFor to inject rules for a single group.

Positioning the style node

Keyframe animations restart whenever the browser re-renders their <style> node.

Place styleNode in a stable part of your DOM — ideally near the root, outside any conditionally-rendered elements or frequently-updating regions.

Event Listeners

Apply events alongside attributes to attach the DOM animation event listeners that drive update.

View Source Code
div
    (Keyframe.attributes "card" model.animState
        ++ Keyframe.events GotAnimMsg
    )
    [ text "Card" ]

Use eventsStopPropagation to prevent events from bubbling to parent elements.

📖 See Render for more info.

Responsive Strategy

Use relative CSS units whenever the motion can be defined in layout-relative terms and the Browser does the work.

For measured pixel targets, Keyframe has no proportional remap API for resize updates because mid-flight values are not available. Therefore:

  • On resize, recompute pixel targets and re-anchor with retarget.
  • retarget snaps immediately to the new correct position and stops.
  • Use this when a layout change makes the old target wrong.

📖 See Responsive Animations for more info.

Playback

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

View Source Code
spinForever =
    Keyframe.loopForever
        >> Keyframe.alternate
        >> Keyframe.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 =
    Keyframe.delay 500
        >> Keyframe.duration 800
        >> Keyframe.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.end

📖 See Timing for more info.

Easing

Keyframe animations support the full Easing library, including bounce and elastic. Complex curves are sampled into densely-spaced @keyframes 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 =
    Keyframe.easing CubicInOut
        >> Keyframe.for "card"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 300
        >> Opacity.delay 50
        >> Opacity.end

📖 See Easing for all available easing functions.

Spring

Keyframe animations support springs. The spring's motion is pre-baked into densely-spaced @keyframes stops, and the browser interpolates linearly between them — visually faithful to the analytic solution.

The motion ends when each value has settled at the target according to the spring settings — there is no explicit duration, therefore any duration or speed settings on the builder are ignored.

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

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

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

Controls

Function Returns Notes
stop AnimState Jump to end state
reset AnimState Jump to start state
restart ( AnimState, Cmd msg ) Reset and play again; Cmd fires Restarted
pause ( AnimState, Cmd msg ) Freeze at current position; Cmd fires Paused
resume ( AnimState, Cmd msg ) Continue from paused position; Cmd fires Resumed
View Source Code
Stop ->
    ( { model | animState = Keyframe.stop "card" model.animState }, Cmd.none )

Reset ->
    ( { model | animState = Keyframe.reset "card" model.animState }, Cmd.none )

Restart ->
    let
        ( animState, cmd ) =
            Keyframe.restart "card" GotAnimMsg model.animState
    in
    ( { model | animState = animState }, cmd )

Pause ->
    let
        ( animState, cmd ) =
            Keyframe.pause "card" GotAnimMsg model.animState
    in
    ( { model | animState = animState }, cmd )

Resume ->
    let
        ( animState, cmd ) =
            Keyframe.resume "card" GotAnimMsg model.animState
    in
    ( { model | animState = animState }, cmd )

📖 See Controlling Animations for more info.

Discrete Properties

The Keyframe engine bakes discrete properties into the generated @keyframes rule. discreteEntry values are emitted on every step, and discreteExit values emit the entry value on all steps, then flip to the exit value on the final frame. 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 =
    Keyframe.for "box"
        >> Keyframe.transformOrder [ Scale, Rotate, Translate ]
        >> Translate.begin
        >> ...

📖 See Transform Order for full details.

State Queries

Query animation state at any time without waiting for events.

View Source Code
Keyframe.anyRunning model.animState           -- Maybe Bool
Keyframe.isRunning "card" model.animState     -- Maybe Bool
Keyframe.allComplete model.animState          -- Maybe Bool
Keyframe.isComplete "card" model.animState    -- Maybe Bool
Keyframe.isCancelled "card" model.animState   -- Maybe Bool

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

Property Queries

CSS keyframes don't provide access to mid-flight values, so only start and end values are tracked.

All query functions follow the same pattern:

  • get[Property]Start, and return Maybe [PropertyValue].
  • get[Property]End, and return Maybe [PropertyValue]
View Source Code
Keyframe.getOpacityStart "card" model.animState    -- Maybe Float
Keyframe.getOpacityEnd "card" model.animState      -- Maybe Float
Keyframe.getTranslateStart "card" model.animState  -- Maybe { x, y, z }
Keyframe.getTranslateEnd "card" model.animState    -- Maybe { x, y, z }

For mid-flight current values, use the Sub or WAAPI engine.

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

When to Choose This Engine

Choose Keyframe when you want browser-native keyframes with state-tracked lifecycle and playback controls.

  • Best for: on-load animations, loops, and timelines that benefit from pause/resume/restart.
  • Avoid when: you need true mid-flight value access or smooth redirection from current playhead position.

API Quick Reference

Types

Type Description
AnimState Tracks animations and their states
AnimBuilder eng Carries all animation configurations
AnimMsg Internal engine messages
AnimEvent Events received during a keyframe animation's lifecycle
AnimGroupName String type alias for the animation group name
CurrentTargetId String type alias for the element that fired the event
TargetId String type alias for the element that owns the listener
TransformProperty Custom transform ordering

Initialize

Function Type Description
init List (AnimBuilder eng -> AnimBuilder eng) -> AnimState Create initial animation state

Trigger

Function Type Description
animate AnimState -> (AnimBuilder eng -> AnimBuilder eng) -> AnimState Apply an animation to the current state

Events

Event Fires when...
Started CurrentTargetId TargetId AnimGroupName Animation begins playing
Ended CurrentTargetId TargetId AnimGroupName Animation completes
Cancelled CurrentTargetId TargetId AnimGroupName Animation is cancelled before completion by an external DOM/CSS change
Iteration CurrentTargetId TargetId AnimGroupName Int Each cycle completes
Paused AnimGroupName pause is called (synthetic)
Resumed AnimGroupName resume is called (synthetic)
Restarted AnimGroupName restart is called (synthetic)

Update

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

View

Function Type Description
attributes AnimGroupName -> AnimState -> List (Html.Attribute msg) Get animation attributes for an element
styleNode AnimState -> Html msg Generate @keyframes rules for all groups
styleNodeFor AnimGroupName -> AnimState -> Html msg Generate @keyframes rules for a specific group
maybeString AnimGroupName -> AnimState -> Maybe String Get the raw @keyframes CSS as a string

Event Listeners

Function Type Description
events (AnimMsg -> msg) -> List (Html.Attribute msg) Attach all animation event listeners
eventsStopPropagation (AnimMsg -> msg) -> List (Html.Attribute msg) Attach all listeners, stops propagation

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
stop AnimGroupName -> AnimState -> AnimState Jump to end state and stop
reset AnimGroupName -> AnimState -> AnimState Jump to start state and stop
restart AnimGroupName -> (AnimMsg -> msg) -> AnimState -> ( AnimState, Cmd msg ) Reset and begin playing again
pause AnimGroupName -> (AnimMsg -> msg) -> AnimState -> ( AnimState, Cmd msg ) Freeze at current position
resume AnimGroupName -> (AnimMsg -> msg) -> AnimState -> ( AnimState, Cmd msg ) Continue from paused position

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

State Queries

Function Type Description
anyRunning AnimState -> Maybe Bool Check if any animation is running
isRunning AnimGroupName -> AnimState -> Maybe Bool Check if a specific group is animating
allComplete AnimState -> Maybe Bool Check if all animations are complete
isComplete AnimGroupName -> AnimState -> Maybe Bool Check if a specific group's animation is complete
isCancelled AnimGroupName -> AnimState -> Maybe Bool Check if a specific group's animation was cancelled

Property Queries

CSS keyframes track only start and end values.

Function Type Description
getOpacityStart AnimGroupName -> AnimState -> Maybe Float Get start opacity
getOpacityEnd AnimGroupName -> AnimState -> Maybe Float Get end opacity
getTranslateStart AnimGroupName -> AnimState -> Maybe { x, y, z } Get start translate
getTranslateEnd AnimGroupName -> AnimState -> Maybe { x, y, z } Get end translate
getRotateStart AnimGroupName -> AnimState -> Maybe { x, y, z } Get start rotate
getRotateEnd AnimGroupName -> AnimState -> Maybe { x, y, z } Get end rotate
getScaleStart AnimGroupName -> AnimState -> Maybe { x, y, z } Get start scale
getScaleEnd AnimGroupName -> AnimState -> Maybe { x, y, z } Get end scale
getSizeStart AnimGroupName -> AnimState -> Maybe { width, height } Get start size
getSizeEnd AnimGroupName -> AnimState -> Maybe { width, height } Get end size
getSkewStart AnimGroupName -> AnimState -> Maybe { x, y } Get start skew
getSkewEnd AnimGroupName -> AnimState -> Maybe { x, y } Get end skew
getPropertyStart AnimGroupName -> String -> AnimState -> Maybe Float Get start value for a custom numeric property
getPropertyEnd AnimGroupName -> String -> AnimState -> Maybe Float Get end value for a custom numeric property
getColorPropertyStart AnimGroupName -> String -> AnimState -> Maybe Color Get start value for a custom color property
getColorPropertyEnd AnimGroupName -> String -> AnimState -> Maybe Color Get end 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.Keyframe documentation.

Next Steps

The Sub Engine which provides a few more features than you get with keyframes.

Sub Engine →