Skip to content

Start here: apply and play

Every animation answers two questions:

  1. Which widget moves? You attach the animation to that widget with .animate(...).
  2. When does it play? A trigger decides: the widget appearing, the pointer, a click, a flag in your state, or a line of your code.

This page explains both, with the same example written in the different styles Pyzed supports. The other pages of this section go deeper into each part.

A login card: the card rises in when it appears, the button lifts under the pointer, a wrong password makes the field shake and a correct one makes a check pop in

An animation lives on the widget it moves. Call .animate() where you build the widget in view(), and use the widget it returns:

def view(self):
return pz.column([
pz.text("Welcome back").animate("fade_in_down"), # this text moves
ui.button("Sign in", self.sign_in).animate("lift", on="hover"),
])

.animate() works on every widget: text, buttons, inputs, containers, rows, icons, images and your own components (which also take animate="fade_in" as an argument). On a container, everything inside moves with it.

You can call .animate() several times: each call adds one animation with its own trigger.

ui.button("Buy", self.buy).animate("fade_in_up").animate("lift", on="hover").animate("shrink", on="press")

The on= option says what plays the animation. You choose one per .animate() call:

You want it to play… Write Plays
when the widget appears .animate("fade_in") (on="mount" is the default) once, each time the widget shows up
while the pointer is over it .animate("lift", on="hover") forward on enter, back on leave
while it is pressed .animate("shrink", on="press") forward while the button is down
while it has the focus .animate("glow", on="focus") forward while focused
on every click on it .animate("tada", on="click") from the start, each click
while a value in your state is true .animate("glow_pulse", active=self.recording) forward while True, back when False
when your code says so .animate("shake", on=self.wrong) + self.wrong.fire() from the start, each fire()
when a value in your state changes .animate("pulse", on="manual", key=self.count) from the start, each new key

The first five need nothing else: Pyzed watches the pointer and the keyboard for you. The last three are how your code starts an animation.

A pz.Trigger is like a button your code presses. Keep one on the app, give it to on= and call fire() in any handler:

class Login(pz.App):
def __init__(self):
self.password = ""
self.wrong = pz.Trigger() # 1. create it once
def view(self):
return pz.column([
ui.input(self.password, self.set_password, secure=True)
.animate("shake", on=self.wrong), # 2. attach: nothing plays yet
ui.button("Sign in", self.sign_in),
], spacing=12)
def set_password(self, text):
self.password = text
def sign_in(self):
if self.password != "secret":
self.wrong.fire() # 3. play it now (again from the start if it was running)

Nothing plays when the widget appears: only fire() starts it. fire ignores its arguments, so it can be a callback itself: ui.button("Shake it", self.wrong.fire).

on="manual" with a key does the same without an object: the animation plays each time the key changes, and never on its own. It is handy with a counter you already have:

ui.badge(str(self.unread)).animate("pulse", on="manual", key=self.unread) # pulses on each new message

A looping animation (repeat="infinite", or a loop preset such as spin) runs while the widget has it. To stop it, stop asking for it in view():

icon = pz.icon("loader-circle")
if self.loading:
icon = icon.animate("spin")

Or use active=, which also plays it back out smoothly: .animate("glow_pulse", active=self.loading).

3. Name it: what you can pass to .animate()

Section titled “3. Name it: what you can pass to .animate()”

The first argument says which animation plays. It can be:

You pass Example Where it comes from
A preset name "fade_in_up", "lift", "spin" The preset gallery: more than 50 ready-made animations, in five groups (entrance, exit, attention, interaction, loop).
A name you defined "wiggle" pz.animation.define_animation("wiggle", ...), once, anywhere before view() runs.
A pz.Animation pz.Animation([{"x": 0}, {"x": 8}]) Your own keyframes, used in place. See Keyframes and easing.
Property values .animate(opacity=0, y=12) A quick animation from (or to) these values. See Animatable properties.
A timeline target self.player["card"] One part of a clip that moves many widgets. See Timeline animations.

Preset names are lowercase words joined by _, and they read like what they do: fade_in_left, slide_out_down, rubber_band. To list them from Python:

from pyzed import animation
print(list(animation.presets())) # every name
print(list(animation.presets("entrance"))) # one group
print(animation.animation("pop_in").description, animation.animation("pop_in").duration)

Every preset brings its own timing; the options of .animate() change it just for that widget:

card.animate("fade_in_up", duration=600, delay=200, easing="ease_out_back")
badge.animate("pulse", repeat=3)
arrow.animate("nudge_right", on="hover", duration=120)

When the change should apply everywhere, define your own animation once and use its name:

from pyzed.animation import define_animation
define_animation("slow_rise", [{"opacity": 0, "y": 40}, {"opacity": 1, "y": 0}],
duration=900, easing="ease_out_cubic")
define_animation("fade_in_up", pz.animation.animation("fade_in_up").with_options(duration=500)) # your own default
pz.text("Hello").animate("slow_rise")

The full list of options (duration, delay, easing, repeat, direction, fill, on_start, on_end…) is in animate() and triggers, and keyframes, easing curves and per-frame easing are in Keyframes and easing.

Pyzed lets you organize your code in several ways. Animations work the same in all of them, because they always end up on a widget returned by a view.

You keep values in your state and view() decides which animations the widgets have. You never “start” anything: you change the state and the screen follows. This is the style to reach for first.

class Player(pz.App):
def __init__(self):
self.playing = False
self.liked = 0
def view(self):
return pz.row([
ui.icon_button("pause" if self.playing else "play", self.toggle)
.animate("glow_pulse", active=self.playing), # glows while playing
ui.icon_button("heart", self.like)
.animate("heartbeat", on="manual", key=self.liked), # beats on every like
pz.text(f"{self.liked} likes").transition(opacity=1 if self.liked else 0.4),
], spacing=8, align="center")
def toggle(self):
self.playing = not self.playing
def like(self):
self.liked += 1

You keep “play” handles and call them from handlers, like button.play() in other toolkits. In Pyzed these handles are pz.Trigger (one widget animation) and pz.AnimationPlayer (a whole clip):

class Checkout(pz.App):
def __init__(self):
self.error = pz.Trigger()
self.done = pz.Trigger()
self.player = pz.AnimationPlayer("checkout.pzanim") # clips saved from Pyzed Design or code
def view(self):
return pz.column([
ui.input(self.card, self.set_card).animate("shake", on=self.error),
pz.icon("circle-check", size=32).animate("pop_in", on=self.done),
pz.text("Thanks!").animate(self.player["thanks"]),
ui.button("Pay", self.pay),
])
def pay(self):
if not self.valid():
self.error.fire()
return
self.done.fire()
self.player.play("celebrate")

A pz.Component keeps its own state, so its triggers live with it (create them in setup()):

class CopyButton(pz.Component):
text: str = ""
def setup(self):
self.copied = pz.Trigger()
def copy(self):
self.copied.fire()
return pz.Command.copy(self.text)
def render(self):
return pz.row([
ui.button("Copy", self.copy, icon="copy"),
pz.text("Copied!").animate("fade_in_up", on=self.copied),
], spacing=8, align="center")

Every component also takes animate= when you place it: CopyButton(text=link, animate="fade_in").

Pieces of a view can be plain functions that receive values and return widgets. Pass them the state (and the triggers) they need: the animation goes on the widget they return.

def status_dot(online: bool) -> pz.Widget:
dot = pz.container(width=10, height=10).style(background="success" if online else "text_muted", radius=5)
return dot.animate("breathe", active=online)
def notification(text: str, index: int) -> pz.Widget:
return ui.callout("New", text).animate("fade_in_left", delay=index * 60)
class Inbox(pz.App):
def view(self):
return pz.column([
pz.row([status_dot(self.online), pz.text("Inbox")], spacing=6, align="center"),
*[notification(text, i) for i, text in enumerate(self.messages)],
], spacing=8)

The editor builds the same animations without code, and shows the line to call when code should start them.

Select a control and open Properties → Animation. Add animation adds a row; click it to choose When it plays and which animation (the presets above):

When (in the editor) Same as in code
When it appears on="mount"
While hovered / pressed / focused on="hover" / "press" / "focus"
On click on="click"
When played from code on=pz.Trigger()

A row set to When played from code waits for your handlers. The row shows the exact line, with a copy button:

def sign_in_click(form, event):
if not check(form.password.text):
form.password.play_animation() # plays its "When played from code" animations

play_animation() also takes any preset, pz.Animation or values, even for a control without rows, and stop_animation() stops what it started (a loop, for example):

form.badge.play_animation("pulse", repeat=3)
form.card.play_animation(scale=1.05, duration=200) # to these values and back
form.spinner.play_animation("spin")
form.spinner.stop_animation()

The Animation panel (View → Animation) edits clips: timelines that move several controls together. A clip starts in one of two ways, and the panel says which one applies:

  • turn on ⚡ Play when the form opens (autoplay), or
  • call it from a handler: form.animations.play("intro") (the panel shows this line with a copy button).

See Animations and effects in Pyzed Design for every field of the editor, and The animation player for play, pause, seek and friends.

I want to… Library Pyzed Design
Animate a widget when it appears w.animate("fade_in_up") Animation → When it appears
React to the pointer w.animate("lift", on="hover") Animation → While hovered
Play on a click on it w.animate("tada", on="click") Animation → On click
Play from a handler on=self.t + self.t.fire() When played from code + form.x.play_animation()
Play while a flag is on w.animate("glow_pulse", active=flag) A loop set to When played from code + play_animation() / stop_animation()
Change a value smoothly w.transition(opacity=...) Style states (hover, pressed)
Move many widgets together pz.AnimationPlayer + player["target"] Animation panel (clips)
Start a clip player.play("intro") ⚡ autoplay or form.animations.play("intro")
Make my own animation pz.Animation([...]), define_animation A clip with tracks and keys