Declarative animations that don't require any messages or subscriptions.
You describe the animation as part of your view and the browser plays it on
its own. There is no Elm update loop involved: no subscriptions, no
Time-based messages, no re-rendering every frame.
The vocabulary used throughout this module:
Here is a small example:
import Evg
import Evg.Animate as Animate
-- Animate a circle's radius with easing:
Evg.circle
[ Evg.fillStr "tomato" ]
{ r = 20, center = ( 50, 50 ) }
|> Animate.animate [ Animate.repeatIndefinite ]
"r"
[ ( 0.5, Animate.easeInOut, "40" )
, ( 1.5, Animate.easeLinear, "20" )
]This shows a red circle that endlessly swells and shrinks back.
The animation repeats forever.
The animation repeats a fixed number of times. Fractional counts are
allowed, so 2.5 runs the animation two and a half times.
The animation keeps repeating until the given number of seconds has elapsed, then stops, even if that means cutting a cycle short.
The total duration of one animation cycle in seconds.
Delay before the animation starts, in seconds.
If you also give a trigger with beginOn, the trigger wins and this delay is
ignored.
When the animation ends, the element stays at its final state rather than snapping back to the start.
Sets the easing (the speed profile of the motion) used between all keyframes.
Animate.keyframes
[ Animate.repeatIndefinite
, Animate.easing Animate.easeInOut
]
[ ( 0
, Evg.circle []
{ r = 10, center = ( 50, 50 ) }
)
, ( 1
, Evg.circle []
{ r = 40, center = ( 50, 50 ) }
)
]This shows a circle growing and shrinking, gently accelerating out of each extreme and slowing into the next.
Jump instantly from one keyframe value to the next with no smooth in-between motion, like a slideshow of frames. Per-keyframe easings are ignored.
Move at a constant speed across the whole animation, spacing the keyframes so that equal distances take equal time. Any keyframe times and easings you gave are ignored in favor of this even pacing.
Add this animation on top of any other transforms already on the element,
rather than replacing them. Transform animations (rotate, scale, and
friends) always do this automatically.
Each repeat builds on the end of the previous one instead of starting over.
For example a translate that moves 10 to the right will end up 20 to the right
after two repeats, 30 after three, and so on.
Start the animation when the given event happens on the element, instead of right away. Give it more than once to start on any of several events.
Animate.animate [ Animate.beginOn Animate.click ]
"r"
[ ( 0.4, Animate.easeOut, "40" ) ]Nothing moves until the shape is clicked; then it quickly grows.
End the animation early when the given event happens on the element.
Something that can start or stop an animation, such as a click or a mouse hover on the animated element itself.
The animation never starts on its own. It waits to be started by another
animation in a sequence.
If the animation is triggered again while it is already running, ignore the new trigger. A fresh trigger only takes effect once the current run has finished.
The animation can only run once; later triggers are ignored.
Send a message the moment the animation starts.
A browser support caveat: browsers only started delivering these animation
milestone events reliably in versions released after late 2025. Older Safari
(including iOS) may never send them, even though the animation itself plays
fine. Don't gate critical state changes on receiving one; if you must support
older browsers, a timer matching the animation's timing (e.g. Process.sleep)
is the dependable alternative. This applies equally to onEnd and onRepeat.
Send a message the moment the animation ends. Never sent by animations that repeat forever.
See onBegin for a browser support caveat: on older browsers (notably
Safari before late 2025) this message may never arrive, so avoid relying on
it for critical state changes. For example, prefer a timer over onEnd for
removing an element after its exit animation.
Send a message each time the animation completes a repeat.
See onBegin for a browser support caveat that also applies here.
The speed profile of the motion between two keyframes: whether it moves at a constant rate, starts slow and speeds up, or the reverse. The values covered are the same either way; easing only changes how fast the animation travels through them at each moment.
Constant speed from start to finish.
A custom speed profile defined by a cubic bezier curve, the same format
used by CSS cubic-bezier(...) timing functions. The four numbers are the two
control points (x1, y1) and (x2, y2).
Animate.cubicBezier 0.68 -0.55 0.265 1.55This one makes the motion pull back slightly, race forward, overshoot the target and settle, like a spring.
Animates a single attribute of a shape between keyframe values with easing.
Each keyframe is a tuple of ( time, easing, value ) where:
time is seconds from the start of the animation when this value is reachedeasing is the speed profile of the motion leading TO this valuevalue is the attribute value at this keyframe (as a string)The initial value comes from the element's own attributes.
-- Pulse a rectangle's corner radius:
Evg.rect [ Evg.cornerRadius 30, Evg.fillStr "navy" ]
{ x = 10, y = 10, width = 100, height = 100 }
|> Animate.animate [ Animate.repeatIndefinite ]
"rx"
[ ( 0.8, Animate.easeLinear, "50" )
, ( 1.8, Animate.easeInOut, "20" )
, ( 3.0, Animate.easeLinear, "30" )
]This shows a navy square whose corners continuously round off and sharpen again.
Instantly sets an attribute to a value at a chosen moment, with no smooth transition. Handy for toggling something on at a delay or on a click.
-- Turn a circle red the moment it is clicked:
Evg.circle
[ Evg.fillStr "gray" ]
{ r = 20, center = ( 50, 50 ) }
|> Animate.set
[ Animate.beginOn Animate.click ]
"fill"
"red"The circle stays gray until clicked, then instantly turns red.
Since nothing is blended, the easing option has no effect here. The
duration option means something different: it is how long the set value
lasts, after which the value snaps back (unless you also use fillFreeze).
Animates between complete shapes at specified times. Each entry is a snapshot of the whole shape at a moment in seconds; the library works out which attributes change across snapshots and generates synchronized animations for each one, and the browser blends between them.
This is the highest-level animation API: you describe what the shape looks like at each moment, and the library handles the rest.
import Evg
import Evg.Animate as Animate
pulsingCircle =
Animate.keyframes
[ Animate.repeatIndefinite
, Animate.easing Animate.easeInOut
]
[ ( 0.0
, Evg.circle
[ Evg.fillStr "rgb(20,60,20)" ]
{ r = 10, center = ( 50, 50 ) }
)
, ( 0.6
, Evg.circle
[ Evg.fillStr "rgb(20,180,20)" ]
{ r = 40, center = ( 50, 50 ) }
)
, ( 1.0
, Evg.circle
[ Evg.fillStr "rgb(20,60,20)" ]
{ r = 10, center = ( 50, 50 ) }
)
]This shows a circle repeatedly growing while brightening to a vivid green, then shrinking and darkening back.
If frames have different element types, the shapes are swapped out at the right moments instead of smoothly blended.
Creates an animation by sampling a function at regular intervals.
The function receives a value from 0.0 to 1.0 representing progress through the animation, and should return the shape at that point in time. The library generates keyframes by evaluating the function at evenly-spaced sample points.
-- A progress bar that fills from left to right:
Animate.sampled [ Animate.repeatIndefinite ]
{ samples = 10, duration = 3 }
(\progress ->
Evg.rect [ Evg.fillStr "green" ]
{ x = 0
, y = 40
, width = progress * 200
, height = 20
}
)This shows a green bar repeatedly stretching from empty to full width over three seconds.
More samples means smoother animation but larger DOM output. For most animations, 6 to 12 samples is sufficient.
Spins a shape by a number of degrees. Each keyframe is
( time, easing, angle ), where angle is in degrees. Positive angles turn
clockwise.
The rotation is added on top of any transforms already on the element, and it
turns around the coordinate system's origin (the top-left corner). To spin a
shape in place, use rotateAround.
-- A shape spinning a full turn every 2 seconds:
Evg.rect
[ Evg.fillStr "teal" ]
{ x = 40, y = 40, width = 20, height = 20 }
|> Animate.rotate [ Animate.repeatIndefinite ]
[ ( 2, Animate.easeLinear, 360 ) ]This shows a square sweeping around the top-left corner of the drawing in a wide circle.
Like rotate, but spins around a chosen center point ( cx, cy ). This
is the usual way to rotate a shape "in place".
-- Spin a square around its center at (50, 50):
Evg.rect
[ Evg.fillStr "teal" ]
{ x = 40, y = 40, width = 20, height = 20 }
|> Animate.rotateAround
[ Animate.repeatIndefinite ]
( 50, 50 )
[ ( 2, Animate.easeLinear, 360 ) ]This shows a square smoothly turning in place, completing a revolution every two seconds.
Grows or shrinks a shape uniformly. A factor of 1 is the original size,
2 is double, 0.5 is half. Each keyframe is ( time, easing, factor ).
-- A pulsing "heartbeat" scale:
Evg.circle
[ Evg.fillStr "crimson" ]
{ r = 20, center = ( 50, 50 ) }
|> Animate.scale [ Animate.repeatIndefinite ]
[ ( 0.3, Animate.easeOut, 1.3 )
, ( 0.6, Animate.easeInOut, 1 )
]This shows a circle throbbing larger and back like a heartbeat.
Note that scaling happens relative to the coordinate system origin, so shapes away from the top-left corner also drift as they grow.
Like scale, but with separate horizontal and vertical factors. Each
keyframe value is a pair ( sx, sy ).
Evg.rect
[ Evg.fillStr "teal" ]
{ x = 40, y = 40, width = 20, height = 20 }
|> Animate.scaleXY [ Animate.repeatIndefinite ]
[ ( 1, Animate.easeInOut, ( 2, 0.5 ) ) ]This shows a square repeatedly squashing into a wide, flat rectangle.
Slides a shape by a horizontal and vertical offset. Each keyframe value is a
pair ( dx, dy ), added on top of the element's own position.
-- Slide 100 to the right and back:
Evg.circle
[ Evg.fillStr "orange" ]
{ r = 10, center = ( 20, 50 ) }
|> Animate.translate
[ Animate.repeatIndefinite ]
[ ( 1, Animate.easeInOut, ( 100, 0 ) )
, ( 2, Animate.easeInOut, ( 0, 0 ) )
]This shows a circle gliding to the right and back again, over and over.
Slants a shape sideways by an angle in degrees (its top edge leans left or
right). Each keyframe is ( time, easing, angle ).
Slants a shape vertically by an angle in degrees (its side edges lean up or
down). Each keyframe is ( time, easing, angle ).
Moves an element along a path over the animation's duration. By default the element also turns to face the direction it is travelling.
import Evg.Path exposing (Path(..), PathSegment(..))
-- A circle repeatedly gliding along a curve:
Evg.circle
[ Evg.fillStr "red" ]
{ r = 5, center = ( 0, 0 ) }
|> Animate.motion
[ Animate.repeatIndefinite
, Animate.duration 3
]
[ M ( 10, 80 )
[ C ( 40, 10 ) ( 65, 10 ) ( 95, 80 ) ]
]This shows a small red dot swooping up and over an arc, then starting again.
Use upright, orientReverse or orientAngle to change how the element is
turned as it moves.
Moves an element along a path like motion, but lets you control how far
along the path the element is at each moment. Each keyframe is
( time, easing, progress ), where progress is a fraction from 0 (start of
the path) to 1 (end).
This lets you speed up, slow down, pause or even reverse along a fixed path.
import Evg.Path exposing (Path(..), PathSegment(..))
Evg.circle
[ Evg.fillStr "red" ]
{ r = 5, center = ( 0, 0 ) }
|> Animate.motionKeyframes
[ Animate.repeatIndefinite
, Animate.duration 4
]
[ M ( 10, 80 )
[ C ( 40, 10 ) ( 65, 10 ) ( 95, 80 ) ]
]
[ ( 1, Animate.easeInOut, 0.5 )
, ( 4, Animate.easeInOut, 1 )
]The dot rushes to the middle of the curve in the first second, then drifts slowly through the rest.
For motion and motionKeyframes: keep the element upright as it travels
along the path, instead of turning to face its direction of movement.
For motion and motionKeyframes: turn the element to face backwards along
its direction of movement.
For motion and motionKeyframes: hold the element at a fixed angle (in
degrees) as it travels, regardless of the path's direction.
Runs a list of animation steps one after another on the same element. Each step waits for the previous one to finish before it starts.
Evg.rect
[ Evg.fillStr "teal" ]
{ x = 40, y = 40, width = 20, height = 20 }
|> Animate.sequence [ Animate.fillFreeze ]
[ Animate.stepTranslate
[ Animate.duration 1 ]
[ ( 1, Animate.easeOut, ( 40, 0 ) ) ]
, Animate.stepRotateAround
[ Animate.duration 1 ]
( 50, 50 )
[ ( 1, Animate.easeInOut, 180 ) ]
]This shows a square sliding to the right, then rotating a half turn once the slide has finished.
If you start the sequence with a trigger (beginOn Animate.click), the whole
chain waits for that event and then runs step by step. Repeating an
event-triggered sequence is not supported.
A single stage of a sequence. Build one with step, stepRotateAround,
stepTranslate or stepScale.
A sequence step that animates a single attribute, exactly like animate.
A sequence step that rotates around a center point, like rotateAround.
A sequence step that slides the element, like translate.
A sequence step that grows or shrinks the element, like scale.
Options that control animation timing, repetition, triggering and how an animation reports back into your program.