CSS Transition Engine¶
This page is a practical guide to using the Transition engine. Read Engines Overview when you want side-by-side comparisons and tradeoffs.
This Engine builds native browser CSS transitions for simple A→B property animations. The browser handles all rendering, providing excellent performance with minimal setup.
Example¶
Simple A→B button hover animations.
View Example
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 ]
Quick Walkthrough¶
Here's a general workflow to get up an running quickly.
1. Build¶
View Source Code
2. Initialize¶
View Source Code
3. Render¶
Render both engine attributes and event listeners on the animated element.
View Source Code
4. Trigger with animate¶
Call animate to apply the animation config to the current AnimState.
View Source Code
5. React¶
Use update for incoming Transition events.
View Source Code
type Msg
= TriggerFadeIn
| GotAnimMsg Transition.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotAnimMsg animMsg ->
let
( animState, maybeEvent ) =
Transition.update animMsg model.animState
in
handleAnimEvent maybeEvent { model | animState = animState }
_ ->
(model, Cmd.none)
handleAnimEvent : Maybe Transition.AnimEvent -> Model -> ( Model, Cmd Msg )
handleAnimEvent maybeEvent model =
case maybeEvent of
Just (Transition.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
📖 See Initialize for more info.
Trigger¶
Call animate to apply an animation to the current AnimState. The browser transitions from its current computed style to the values provided.
Starting values in the builder config are ignored — the browser always starts CSS Transitions from the element's current computed style, and the Engine cannot override this.
View Source Code
📖 See Triggering Animations for more info.
Mid-Flight Interruptions¶
Because the browser starts from current computed style, interrupting an animation mid-flight automatically transitions smoothly from wherever the element is — just provide a new end value and re-trigger with animate.
📖 See Interrupting Animations for more info.
OnLoad Animations¶
If a transition must run immediately on page load, defer triggering to the next event loop tick with Process.sleep 0. This lets the initial render commit first so the browser has a starting computed style to transition from. Without it, the browser sees only the end value on first paint and moves immediately to it, with no animation.
View Source Code
Update¶
Use update to process incoming transition messages. It returns the updated AnimState and a Maybe AnimEvent.
View Source Code
Events¶
update returns a single Maybe AnimEvent per call. Each event carries 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.
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.
View Source Code
| Event | Fires when... |
|---|---|
Run |
Transition is queued to run (before any delay) |
Started |
Transition begins playing |
Ended |
Transition completes |
Cancelled |
Transition is interrupted by something outside the engine's control. |
📖 See React for more info.
View¶
Apply attributes to the animated element to set its transition rules and inline styles.
Event Listeners¶
Apply events alongside attributes to attach the DOM transition event listeners that drive update.
View Source Code
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, Transition has no proportional remap API for resize updates because mid-flight values are not available. Therefore:
- On resize, recompute pixel targets and re-trigger with
animate. - Running animations then continue smoothly from current computed style to the new target.
- If a later layout change makes the old target wrong, use
retargetto snap straight to the new correct position.
📖 See Responsive Animations for more info.
Timing¶
Set the default duration, speed, and delay. Inherited by every property that doesn't override them.
duration— animation length in milliseconds.speed— alternative toduration; set a rate in property units per second.delay— wait before the transition begins, in milliseconds.
View Source Code
📖 See Timing for more info.
Easing¶
Easings are converted to CSS cubic-bezier values for the browser to render natively.
Most standard easings (sine, quad, cubic, quart, quint, expo) convert accurately. However, complex curves like bounce and elastic are approximated and won't match their mathematical definitions exactly.
For accurate complex easing curves, use the Keyframe, Sub, or WAAPI engine instead.
Set the default easing for all properties that don't override it:
View Source Code
📖 See Easing for all available easing functions.
Controls¶
stop jumps the animation to its end state. reset jumps to the start state.
View Source Code
📖 See Controlling Animations for more info.
Discrete Properties¶
The Transition engine uses discreteEntry and discreteExit — the same API as all other engines.
For this engine, calling either function enables the browser's native transition-behavior: allow-discrete CSS feature.
For entry animations, include startingStyleNode in your view. This generates @starting-style CSS rules so the browser knows the interpolable property values to animate from when an element first appears. Without it, entry transitions are skipped.
View Source Code
fadeIn : AnimBuilder eng -> AnimBuilder eng
fadeIn =
Opacity.begin
>> Opacity.to 1
>> Opacity.end
fadeOut : AnimBuilder eng -> AnimBuilder eng
fadeOut =
Opacity.begin
>> Opacity.to 0
>> Opacity.end
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FadeBoxIn ->
({ model | animState =
Transition.animate model.animState <|
Transition.for "box"
>> Transition.discreteEntry "display" "block"
>> fadeIn
}
, Cmd.none
)
FadeBoxOut ->
({ model | animState =
Transition.animate model.animState <|
Transition.for "box"
>> Transition.discreteExit "display" "block" "none"
>> fadeOut
}
, Cmd.none
)
view : Model -> Html Msg
view model =
div []
[ Transition.startingStyleNode model.animState
, div
(Transition.attributes "box" model.animState
++ Transition.events GotAnimMsg
++ [ style "display" "none" ]
)
[ text "Hello!" ]
]
Browser Support
transition-behavior: allow-discrete requires modern browsers (Chrome 117+, Firefox 129+, Safari 18+). In older browsers, discrete property transitions won't animate — the property will change instantly.
📖 See Discrete Properties for the full API, live examples, and source code.
State Queries¶
Query animation state.
View Source Code
Nothing is returned when no animation exists.
Property Queries¶
No start or current values are queryable for CSS Transitions, so all that is available are the end values provided in the animation builders.
All query functions follow the same pattern: get[Property]End, and return Maybe [PropertyValue].
View Source Code
Nothing is returned when no animation exists for the given group.
When to Choose This Engine¶
Choose Transition when you want minimal setup and smooth A→B animations.
- Best for: UI interactions, hovers, toggles, and small component transitions.
- Avoid when: you need richer playback control or mid-flight visibility.
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 transition'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 |
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 | Description |
|---|---|
Run CurrentTargetId TargetId AnimGroupName |
Transition is queued to run |
Started CurrentTargetId TargetId AnimGroupName |
Transition begins playing |
Ended CurrentTargetId TargetId AnimGroupName |
Transition completes |
Cancelled CurrentTargetId TargetId AnimGroupName |
Transition is cancelled |
Update¶
| Function | Type | Description |
|---|---|---|
update |
AnimMsg -> AnimState -> (AnimState, Maybe AnimEvent) |
Process transition messages |
View¶
| Function | Type | Description |
|---|---|---|
attributes |
AnimGroupName -> AnimState -> List (Html.Attribute msg) |
Get transition attributes for an element |
Event Listeners¶
| Function | Type | Description |
|---|---|---|
events |
(AnimMsg -> msg) -> List (Html.Attribute msg) |
Attach all transition event listeners |
eventsStopPropagation |
(AnimMsg -> msg) -> List (Html.Attribute msg) |
Attach all listeners, stops propagation |
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 transition starts (ms) |
Easing¶
| Function | Type | Description |
|---|---|---|
easing |
Easing -> AnimBuilder eng -> AnimBuilder eng |
Set easing function |
Controls¶
| Function | Type | Description |
|---|---|---|
stop |
AnimGroupName -> AnimState -> AnimState |
Jump to end state and stop |
reset |
AnimGroupName -> AnimState -> AnimState |
Jump to start state and stop |
Discrete Properties¶
| Function | Type | Description |
|---|---|---|
discreteEntry |
String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a discrete CSS property value for entry animations |
discreteExit |
String -> String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a discrete CSS property value for exit animations |
startingStyleNode |
AnimState -> Html msg |
Generate @starting-style rules for all groups |
startingStyleNodeFor |
AnimGroupName -> AnimState -> Html msg |
Generate @starting-style rules for a specific group |
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 transitions track only end values.
| Function | Type | Description |
|---|---|---|
getOpacityEnd |
AnimGroupName -> AnimState -> Maybe Float |
Get end opacity |
getTranslateEnd |
AnimGroupName -> AnimState -> Maybe { x, y, z } |
Get end translate |
getRotateEnd |
AnimGroupName -> AnimState -> Maybe { x, y, z } |
Get end rotate |
getScaleEnd |
AnimGroupName -> AnimState -> Maybe { x, y, z } |
Get end scale |
getSizeEnd |
AnimGroupName -> AnimState -> Maybe { width, height } |
Get end size |
getSkewEnd |
AnimGroupName -> AnimState -> Maybe { x, y } |
Get end skew |
getPropertyEnd |
AnimGroupName -> String -> AnimState -> Maybe Float |
Get end value for a custom numeric property |
getColorPropertyEnd |
AnimGroupName -> String -> AnimState -> Maybe Color |
Get end value for a custom color property |
For complete API details, see the Anim.Engine.Transition documentation.
Next Steps¶
The Keyframe Engine which provides a few different features to what you get with transitions.