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
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.