Discrete Properties¶
Most CSS properties like opacity, transform, and background-color can have intermediate values, so the browser can smoothly interpolate between start and end. In CSS terms, these are interpolable values.
Some properties or value pairs animate with discrete behavior, which means there are no in between states and values change instantly at defined points. For example, there is no halfway point between display: none and display: flex.
In this documentation, "discrete properties" refers to CSS properties and values that use discrete animation behavior, typically keyword based values such as display, visibility, content-visibility, or height: auto.
This matters for animations because you often want to show or hide an element with a smooth fade, but the display property change happens instantly. Without discrete property support, the element either disappears before the fade completes, or appears without any transition at all.
All four animation engines support discrete properties through a unified API.
Example¶
All four examples use display as a discrete property combined with an opacity fade. Click Show to fade in (setting display: flex on the first frame), and Hide to fade out (setting display: none on the last frame).
View Examples
View Source Code
module Animation.Transition.DiscreteProperties.Main exposing (main)
import Anim.Engine.Transition as Transition exposing (AnimBuilder)
import Anim.Property.Opacity as Opacity
import Browser
import Html exposing (Html, button, div, p, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as 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
[ Transition.discreteEntry "display" "none"
>> Opacity.init animGroup 0
]
}
, Cmd.none
)
-- ANIMATION
animGroup : String
animGroup =
"fadeAnim"
fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
Opacity.begin
>> Opacity.to 1
>> Opacity.duration 800
>> Opacity.easing QuartIn
>> Opacity.end
fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
Opacity.begin
>> Opacity.to 0
>> Opacity.duration 800
>> Opacity.easing CubicIn
>> Opacity.end
-- UPDATE
type Msg
= Show
| Hide
| GotAnimMsg Transition.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Show ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroup
>> Transition.discreteEntry "display" "flex"
>> fadeIn
}
, Cmd.none
)
Hide ->
( { model
| animState =
Transition.animate model.animState <|
Transition.for animGroup
>> Transition.discreteExit "display" "flex" "none"
>> fadeOut
}
, Cmd.none
)
GotAnimMsg animMsg ->
let
( newAnimState, _ ) =
Transition.update animMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div [ class "example-stage" ]
[ Transition.startingStyleNode model.animState
, div [ class "example-controls" ]
[ button
[ onClick Show
, class "ui-action-button primary"
]
[ text "Show" ]
, button
[ onClick Hide
, class "ui-action-button primary"
]
[ text "Hide" ]
]
, p
[ style "color" "#666"
, style "font-size" "13px"
, style "text-align" "center"
, style "margin" "0"
]
[ text "Uses discreteEntry/discreteExit to flip display on first/last frames." ]
, div
(Transition.attributes animGroup model.animState
++ Transition.events GotAnimMsg
++ [ class "example-box"
, style "background-color" "#4a90d9"
, style "border-radius" "12px"
, style "align-items" "center"
, style "justify-content" "center"
, style "color" "white"
, style "font-size" "18px"
, style "font-weight" "bold"
]
)
[ text "Hello!" ]
]
module Animation.Keyframe.DiscreteProperties.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, p, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Motion.Easing as 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
[ Keyframe.discreteEntry "display" "none"
>> Opacity.init animGroup 0
]
}
, Cmd.none
)
-- ANIMATION
animGroup : String
animGroup =
"fadeAnim"
fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
Opacity.begin
>> Opacity.from 0
>> Opacity.to 1
>> Opacity.duration 800
>> Opacity.easing QuartIn
>> Opacity.end
fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
Opacity.begin
>> Opacity.from 1
>> Opacity.to 0
>> Opacity.duration 800
>> Opacity.easing CubicIn
>> Opacity.end
-- UPDATE
type Msg
= Show
| Hide
| GotAnimMsg Keyframe.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Show ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroup
>> Keyframe.discreteEntry "display" "flex"
>> fadeIn
}
, Cmd.none
)
Hide ->
( { model
| animState =
Keyframe.animate model.animState <|
Keyframe.for animGroup
>> Keyframe.discreteExit "display" "flex" "none"
>> fadeOut
}
, Cmd.none
)
GotAnimMsg animMsg ->
let
( newAnimState, _ ) =
Keyframe.update animMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div [ class "example-stage" ]
[ Keyframe.styleNode model.animState
, div [ class "example-controls" ]
[ button
[ onClick Show
, class "ui-action-button primary"
]
[ text "Show" ]
, button
[ onClick Hide
, class "ui-action-button primary"
]
[ text "Hide" ]
]
, p
[ style "color" "#666"
, style "font-size" "13px"
, style "text-align" "center"
, style "margin" "0"
]
[ text "Uses discreteEntry/discreteExit to flip display on first/last frames." ]
, div
(Keyframe.attributes animGroup model.animState
++ Keyframe.events GotAnimMsg
++ [ class "example-box"
, style "background-color" "#4a90d9"
, style "border-radius" "12px"
, style "align-items" "center"
, style "justify-content" "center"
, style "color" "white"
, style "font-size" "18px"
, style "font-weight" "bold"
]
)
[ text "Hello!" ]
]
module Animation.Sub.DiscreteProperties.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, p, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
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
[ Sub.discreteEntry "display" "none"
>> Opacity.init animGroup 0
]
}
, Cmd.none
)
-- ANIMATION
animGroup : String
animGroup =
"fadeAnim"
fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
Opacity.begin
>> Opacity.to 1
>> Opacity.duration 800
>> Opacity.easing QuartIn
>> Opacity.end
fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
Opacity.begin
>> Opacity.to 0
>> Opacity.duration 800
>> Opacity.easing CubicIn
>> Opacity.end
-- UPDATE
type Msg
= Show
| Hide
| GotSubMsg Sub.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Show ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroup
>> Sub.discreteEntry "display" "flex"
>> fadeIn
}
, Cmd.none
)
Hide ->
( { model
| animState =
Sub.animate model.animState <|
Sub.for animGroup
>> Sub.discreteExit "display" "flex" "none"
>> fadeOut
}
, Cmd.none
)
GotSubMsg subMsg ->
let
( newAnimState, _ ) =
Sub.update subMsg model.animState
in
( { model | animState = newAnimState }
, 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" ]
[ div [ class "example-controls" ]
[ button
[ onClick Show
, class "ui-action-button primary"
]
[ text "Show" ]
, button
[ onClick Hide
, class "ui-action-button primary"
]
[ text "Hide" ]
]
, p
[ style "color" "#666"
, style "font-size" "13px"
, style "text-align" "center"
, style "margin" "0"
]
[ text "Uses discreteEntry/discreteExit to flip display on first/last frames." ]
, div
(Sub.attributes animGroup model.animState
++ [ class "example-box"
, style "background-color" "#4a90d9"
, style "border-radius" "12px"
, style "align-items" "center"
, style "justify-content" "center"
, style "color" "white"
, style "font-size" "18px"
, style "font-weight" "bold"
]
)
[ text "Hello!" ]
]
port module Animation.WAAPI.DiscreteProperties.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, p, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (onClick)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
-- 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 <|
[ WAAPI.discreteEntry "display" "none"
>> Opacity.init animGroup 0
]
}
, Cmd.none
)
-- ANIMATION
animGroup : String
animGroup =
"fadeAnim"
fadeIn : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeIn =
Opacity.begin
>> Opacity.to 1
>> Opacity.duration 800
>> Opacity.easing QuartIn
>> Opacity.end
fadeOut : AnimBuilder { eng | withTiming : () } -> AnimBuilder { eng | withTiming : () }
fadeOut =
Opacity.begin
>> Opacity.to 0
>> Opacity.duration 800
>> Opacity.easing CubicIn
>> Opacity.end
-- UPDATE
type Msg
= Show
| Hide
| GotWaapiMsg WAAPI.AnimMsg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Show ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroup
>> WAAPI.discreteEntry "display" "flex"
>> fadeIn
in
( { model
| animState = newAnimState
}
, cmd
)
Hide ->
let
( newAnimState, cmd ) =
WAAPI.animate model.animState <|
WAAPI.for animGroup
>> WAAPI.discreteExit "display" "flex" "none"
>> fadeOut
in
( { model
| animState = newAnimState
}
, cmd
)
GotWaapiMsg waapiMsg ->
let
( newAnimState, _ ) =
WAAPI.update waapiMsg model.animState
in
( { model | animState = newAnimState }
, Cmd.none
)
-- 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 Show
, class "ui-action-button primary"
]
[ text "Show" ]
, button
[ onClick Hide
, class "ui-action-button primary"
]
[ text "Hide" ]
]
, p
[ style "color" "#666"
, style "font-size" "13px"
, style "text-align" "center"
, style "margin" "0"
]
[ text "Uses discreteEntry/discreteExit to flip display on first/last frames." ]
, div
(WAAPI.attributes animGroup model.animState
++ [ class "example-box"
, style "background-color" "#4a90d9"
, style "border-radius" "12px"
, style "align-items" "center"
, style "justify-content" "center"
, style "color" "white"
, style "font-size" "18px"
, style "font-weight" "bold"
]
)
[ text "Hello!" ]
]
discreteEntry¶
Sets a CSS property value when the animation starts. Use this when an element is appearing — for example, going from display: none to display: flex while fading in.
View Source Code
The value is applied from the first frame and held throughout the animation.
In init¶
To set a discrete property as part of the initial state, include discreteEntry in your init pipeline:
View Source Code
This tells the engine what value to apply at the initial render and at the start of entry animations. Here, the element will render with display: flex and opacity: 1 as its initial visible state.
discreteExit¶
Sets a CSS property value for exit animations. It holds the from value during the animation and flips to the to value when the animation ends. Use this when an element is disappearing — for example, fading out and then setting display: none.
View Source Code
The three arguments are: property name, value during animation, value after animation ends.
API Reference¶
| Function | Type | Description |
|---|---|---|
discreteEntry |
String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a CSS discrete property value when the animation starts |
discreteExit |
String -> String -> String -> AnimBuilder eng -> AnimBuilder eng |
Set a CSS discrete property value during and after the animation |
Next Steps¶
Now that you've learnt about the Engines and Properties, learn about Engine Capabilities.