Skip to content

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
fadeIn =
    Transition.discreteEntry "display" "flex"
        >> Transition.for "box"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 800
        >> Opacity.end
fadeIn =
    Keyframe.discreteEntry "display" "flex"
        >> Keyframe.for "box"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 800
        >> Opacity.end
fadeIn =
    Sub.discreteEntry "display" "flex"
        >> Sub.for "box"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 800
        >> Opacity.end
fadeIn =
    WAAPI.discreteEntry "display" "flex"
        >> WAAPI.for "box"
        >> Opacity.begin
        >> Opacity.to 1
        >> Opacity.duration 800
        >> Opacity.end

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
init =
    ( { animState =
            Transition.init
                [ Transition.discreteEntry "display" "flex"
                    >> Opacity.init "box" 1
                ]
    }
    , Cmd.none
    )
init =
    ( { animState =
            Keyframe.init
                [ Keyframe.discreteEntry "display" "flex"
                    >> Opacity.init "box" 1
                ]
    }
    , Cmd.none
    )
init =
    ( { animState =
            Sub.init
                [ Sub.discreteEntry "display" "flex"
                    >> Opacity.init "box" 1
                ]
    }
    , Cmd.none
    )
init =
    ( { animState =
            WAAPI.init motionCmd motionMsg <|
                [ WAAPI.discreteEntry "display" "flex"
                    >> Opacity.init "box" 1
                ]
    }
    , Cmd.none
    )

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
fadeOut =
    Transition.discreteExit "display" "flex" "none"
        >> Transition.for "box"
        >> Opacity.begin
        >> Opacity.to 0
        >> Opacity.duration 800
        >> Opacity.end
fadeOut =
    Keyframe.discreteExit "display" "flex" "none"
        >> Keyframe.for "box"
        >> Opacity.begin
        >> Opacity.to 0
        >> Opacity.duration 800
        >> Opacity.end
fadeOut =
    Sub.discreteExit "display" "flex" "none"
        >> Sub.for "box"
        >> Opacity.begin
        >> Opacity.to 0
        >> Opacity.duration 800
        >> Opacity.end
fadeOut =
    WAAPI.discreteExit "display" "flex" "none"
        >> WAAPI.for "box"
        >> Opacity.begin
        >> Opacity.to 0
        >> Opacity.duration 800
        >> Opacity.end

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.

Engine Capabilities →