Skip to content

Motion

Partas.Solid.Motion binds the solid-motionone library, and the animate function from motion.

solid-motionone

See the docs for usage.

solid-motionone uses different prop names from motion/react: keyframes in transition use offset, not times (see Keyframes).

Differences

Each Motion-specific prop has two forms:

  • The plain name, such as initial, takes a Feliz-style list of props. Build it with the MotionStyle qualifier for CSS props such as animate and initial, and MotionTransition for transition, which enforces the AnimationOptions Pojo when you set a CSS property inside it.
  • The JSX-suffixed name, such as initialJSX, takes the value as a JSX string (see Language injection).

motion

See the docs for usage. Only the animate function is bound.

Example

A component that cycles through words:

[<Erase>]
type WordRotate() =
    inherit div()
    interface OptionKeys
    [<Erase>] member val words: string[] = unbox null with get,set
    [<Erase>] member val duration: int = unbox null with get,set
    [<SolidTypeComponent>]
    member props.constructor =
        props.duration <- 2500 // set a default duration if none is set
        let index,setIndex = createSignal(0)
        createEffect(
            fun () ->
                let interval = setInterval (fun () -> setIndex.Invoke(fun prevIndex -> (prevIndex + 1) % (props.words.Length))) props.duration
                onCleanup(fun () -> clearInterval(interval))
        )
        Presence(exitBeforeEnter = true) {
            Show(when' = !!(index() + 1), keyed = true) {
                Motion(
                    initial = [
                        MotionStyle.opacity 0
                        MotionStyle.y -50
                    ],
                    animate = [
                        MotionStyle.opacity 1
                        MotionStyle.y 0
                    ],
                    exit = [
                        MotionStyle.opacity 0
                        MotionStyle.y 50
                    ],
                    transition = [
                        MotionTransition.duration 0.25
                        MotionTransition.easing Easing.EaseOut
                    ]
                ).spread props {
                    props.words[index()]
                }
            }
        }
[<SolidComponent>]
let WordRotateExample () =
    div() {
        WordRotate(style = "font-size: 4rem",
                   words = [| "Word" ; "Rotate" |])
    }

The word fades and slides out downwards, and the next one fades in from above, every 2.5 seconds.

Live example on Solid 2

Until the bindings are ported, import the framework-free motion-dom, the DOM engine underneath Motion. Its animateElement takes an element, a keyframes object and a transition object. The tupled parameters make Fable emit a normal call, and anonymous records compile to the plain objects Motion expects.

type PlaybackControls =
    abstract stop: unit -> unit

[<Import("animateElement", "motion-dom")>]
let animateElement (element: obj, keyframes: obj, transition: obj): PlaybackControls array =
    jsNative

[<SolidComponent>]
let WordRotateDemo () =
    let words = [| "typed"; "reactive"; "fine-grained"; "just F#" |]
    let index, setIndex = createSignal 0
    let mutable word: Browser.Types.HTMLSpanElement = JS.undefined

    onSettled (fun () ->
        let id = JS.setInterval (fun () -> setIndex ((index () + 1) % words.Length)) 2000
        fun () -> JS.clearInterval id)

    createEffect (
        (fun (_: int option) -> index ()),
        fun (_: int) ->
            let running =
                animateElement (
                    word,
                    {| opacity = ResizeArray [ 0.; 1. ]
                       y = ResizeArray [ 14; 0 ]
                       filter = ResizeArray [ "blur(6px)"; "blur(0px)" ] |},
                    {| duration = 0.45; ease = ResizeArray [ 0.22; 1.; 0.36; 1. ] |}
                )
            fun () -> running |> Array.iter (fun a -> a.stop ())
    )

    div (style = "font-size: 1.75rem; font-weight: 600; color: var(--nacara-heading)") {
        "Partas.Solid is "
        span(style = "display: inline-block; color: var(--nacara-primary)").ref (word) {
            words[index ()]
        }
    }
export function WordRotateDemo() {
    const words = ["typed", "reactive", "fine-grained", "just F#"];
    const patternInput = createSignal(0);
    const index = patternInput[0];
    let word = void 0;
    onSettled(() => {
        const id = setInterval(() => {
            patternInput[1]((index() + 1) % words.length);
        }, 2000) | 0;
        return () => {
            clearInterval(id);
        };
    });
    createEffect((_arg) => (index() | 0), (_arg_1) => {
        const running = animateElement(word, {
            filter: ["blur(6px)", "blur(0px)"],
            opacity: [0, 1],
            y: [14, 0],
        }, {
            duration: 0.45,
            ease: [0.22, 1, 0.36, 1],
        });
        return () => {
            running.forEach((a) => {
                a.stop();
            });
        };
    });
    return <div style="font-size: 1.75rem; font-weight: 600; color: var(--nacara-heading)">
        {"Partas.Solid is "}
        <span style="display: inline-block; color: var(--nacara-primary)"
            ref={word}>
            {item(index(), words)}
        </span>
    </div>;
}

The effect tracks index and replays the enter animation each time the word changes, returning a cleanup that stops the playback controls animateElement returns. Return a real cleanup: Solid 2 calls whatever the effect function returns, and animateElement (...) |> ignore still compiles to an arrow that returns the controls array, which then fails as "not a function".

The keyframes use ResizeArray rather than [| ... |]. Fable compiles an F# float[] or int[] to a typed array such as Float64Array, and Motion only treats a real JS array as a list of keyframes or a cubic-bezier ease.

Keyframes

[<SolidComponent>]
let KeyframeExample () =
    let inline percent value = $"{value}%%"
    Motion.div(
        animate = [
            MotionStyle.scale !^[|1;2;2;1;1|]
            MotionStyle.rotate !^[|0;0;180;180;0|]
            MotionStyle.borderRadius !^[|percent 0; percent 0; percent 50; percent 50; percent 0|]
        ],
        transition = [
            MotionTransition.duration 2
            MotionTransition.easing Easing.EaseInOut
            MotionTransition.offset [|0.;0.2;0.5;0.8;1|]
            MotionTransition.repeat JS.Infinity
            MotionTransition.endDelay 1.
        ],
        style="width: 100px; height: 100px; background-color: green"
        )

A green square scales up, turns, rounds into a circle, and returns, on repeat.

Computation expression helpers

In Partas.Solid 2.x, createEffect could also be written with the experimental computation expressions:

createEffect(
    fun () ->
        let interval =
            setInterval (fun () -> setIndex.Invoke(fun prevIndex -> (prevIndex + 1) % (props.words.Length))) props.duration
        onCleanup(fun () -> clearInterval(interval))
)
effect {
    let interval = setInterval (fun _ -> setIndex.Invoke(fun prevIndex -> (prevIndex + 1) % props.words.Length)) props.duration
    cleanup { clearInterval(interval) }
}

In 3.0 the effect { } builder targets the two-phase createEffect and needs exactly one let! for the tracked value. See Experimental.

Language injection

The JSX-suffixed props give you JSX language injection in JetBrains IDEs:

Presence(exitBeforeEnter = true) {
    Show(when' = !!(index() + 1), keyed = true) {
        Motion(
            initialJSX = "{ opacity: 0, y: -50 }",
            animateJSX = "{ opacity: 1, y: 0 }",
            exitJSX = "{ opacity: 0, y: 50 }",
            transitionJSX = "{ duration: 0.25, easing: 'ease-out' }"
        ).spread props {
            props.words[index()]
        }
    }
}
Edit this page