Transition

Drives a Vue-compatible six-class CSS protocol around a single conditional child: the framework owns class timing and delayed DOM removal while users supply plain CSS.

Transition

Demo
Code
from webcompy.app import WebComPyApp
from webcompy.components import ComponentContext, define_component
from webcompy.elements import Transition, html
from webcompy.signal import use_state


@define_component("transition-demo-app")
def TransitionDemo(_: ComponentContext[None]):
    fade_visible = use_state(lambda: False)
    slide_visible = use_state(lambda: False)

    def _toggle_fade(_ev):
        fade_visible.value = not fade_visible.value

    def _toggle_slide(_ev):
        slide_visible.value = not slide_visible.value

    return html.DIV(
        {"class": "transition-demo"},
        html.H1({}, "Transition Demo"),
        html.P({}, "Show and hide content; the CSS classes are driven by WebComPy."),
        html.DIV(
            {"class": "demo-controls"},
            html.BUTTON({"id": "toggle-fade", "@click": _toggle_fade}, "Toggle Fade"),
            html.BUTTON({"id": "toggle-slide", "@click": _toggle_slide}, "Toggle Slide"),
        ),
        Transition(
            {"name": "fade"},
            lambda: html.DIV({"class": "demo-fade-box"}, "Fade content") if fade_visible.value else None,
        ),
        Transition(
            {"name": "slide"},
            lambda: html.DIV({"class": "demo-slide-box"}, "Slide content") if slide_visible.value else None,
        ),
    )


TransitionDemo.scoped_style = {
    ".transition-demo": {
        "font-family": "sans-serif",
        "padding": "1rem",
    },
    ".demo-controls": {
        "display": "flex",
        "gap": "0.5rem",
        "margin-top": "1rem",
    },
    ".demo-controls button": {
        "padding": "0.5rem 1rem",
        "cursor": "pointer",
    },
    ".demo-fade-box": {
        "margin-top": "1rem",
        "padding": "1rem",
        "background": "mistyrose",
        "transition": "opacity 400ms ease",
    },
    ".fade-enter-from": {
        "opacity": "0",
    },
    ".fade-enter-active": {
        "transition": "opacity 400ms ease",
    },
    ".fade-enter-to": {
        "opacity": "1",
    },
    ".fade-leave-from": {
        "opacity": "1",
    },
    ".fade-leave-active": {
        "transition": "opacity 400ms ease",
    },
    ".fade-leave-to": {
        "opacity": "0",
    },
    ".demo-slide-box": {
        "margin-top": "1rem",
        "padding": "1rem",
        "background": "lightcyan",
        "transform": "translateX(0)",
    },
    ".slide-enter-from": {
        "transform": "translateX(100%)",
    },
    ".slide-enter-active": {
        "transition": "transform 400ms ease",
    },
    ".slide-enter-to": {
        "transform": "translateX(0)",
    },
    ".slide-leave-from": {
        "transform": "translateX(0)",
    },
    ".slide-leave-active": {
        "transition": "transform 400ms ease",
    },
    ".slide-leave-to": {
        "transform": "translateX(-100%)",
    },
}

app = WebComPyApp(root_component=TransitionDemo)
app.run()

Class protocol

Wrap a generator with Transition({"name": "fade"}, generator). When the child appears the framework applies fade-enter-from, swaps to fade-enter-active and fade-enter-to on the next frame, and removes all three classes when the enter duration elapses. Disappearance is intercepted: fade-leave-from, then fade-leave-active + fade-leave-to, and only then is the node removed.

Duration resolution

The duration resolves from the explicit duration prop (milliseconds) first, then from the longest computed transition/animation duration of the node. When neither yields a duration the sequence finishes immediately with a warning. prefers-reduced-motion skips all sequences.

Single child

The generator yields at most one element (or None); replacing one visible child with another runs leave first, then enter. A content update re-yielding the same element type patches the node in place without restarting a running sequence (during a leave, the update waits for the leave to finish, then remounts and enters). Initial renders, SSR, and hydration show the steady state without an appear animation.