Skip to content

Slide Transitions

Under construction!

Transitions are a new and experiment feature in Spiel and the interface might change dramatically from version to version. If you plan on using transitions, we recommend pinning the exact version of Spiel your presentation was developed in to ensure stability.

Setting Transitions

To set the default transition for the entire deck, which will be used if a slide does not override it, set Deck.default_transition to a type that implements the Transition protocol.

For example, the default transition is Swipe, so not passing default_transition at all is equivalent to

from spiel import Deck, Swipe

deck = Deck(name=f"Spiel Demo Deck", default_transition=Swipe)

To override the deck-wide default for an individual slide, specify the transition type in the @slide decorator:

from spiel import Deck, Swipe

deck = Deck(name=f"Spiel Demo Deck")

@deck.slide(title="My Title", transition=Swipe)
def slide():
    ...

Or, in the arguments to Slide:

from spiel import Slide, Swipe

slide = Slide(title="My Title", transition=Swipe)

In either case, the specified transition will be used when transitioning to that slide. It does not matter whether the slide is the "next" or "previous" slide: the slide being moved to determines which transition effect will be used.

Disabling Transitions

In any of the above examples, you can also set default_transition/transition to None. In that case, there will be no transition effect when moving to the slide; it will just be displayed on the next render, already in-place.

Writing Custom Transitions

To implement your own custom transition, you must write a class which implements the Transition protocol.

The protocol is:

Transition Protocol
from __future__ import annotations

from enum import Enum
from typing import Protocol, runtime_checkable

from textual.widget import Widget


class Direction(Enum):
    """
    An enumeration that describes which direction a slide transition
    animation should move in: whether we're going to the next slide,
    or to the previous slide.
    """

    Next = "next"
    """Indicates that the transition should handle going to the next slide."""

    Previous = "previous"
    """Indicates that the transition should handle going to the previous slide."""


@runtime_checkable
class Transition(Protocol):
    """
    A protocol that describes how to implement a transition animation.

    See [Writing Custom Transitions](./transitions.md#writing-custom-transitions)
    for more details on how to implement the protocol.
    """

    def initialize(
        self,
        from_widget: Widget,
        to_widget: Widget,
        direction: Direction,
    ) -> None:
        """
        A hook function to set up any CSS that should be present at the start of the transition.

        Args:
            from_widget: The widget showing the slide that we are leaving.
            to_widget: The widget showing the slide that we are entering.
            direction: The desired direction of the transition animation.
        """
        ...

    def progress(
        self,
        from_widget: Widget,
        to_widget: Widget,
        direction: Direction,
        progress: float,
    ) -> None:
        """
        A hook function that is called each time the `progress`
        of the transition animation updates.

        Args:
            from_widget: The widget showing the slide that we are leaving.
            to_widget: The widget showing the slide that we are entering.
            direction: The desired direction of the transition animation.
            progress: The progress of the animation, as a percentage
                (e.g., initial state is `0`, final state is `100`).
                Note that this is **not necessarily** bounded between `0` and `100`,
                nor is it necessarily [monotonically increasing](https://en.wikipedia.org/wiki/Monotonic_function),
                depending on the underlying Textual animation easing function,
                which may overshoot or bounce.
                However, it will always start at `0` and end at `100`,
                no matter which `direction` the transition should move in.
        """
        ...

As an example, consider the Swipe transition included in Spiel:

Swipe Transition
from __future__ import annotations

from textual.widget import Widget

from spiel.transitions.protocol import Direction, Transition


class Swipe(Transition):
    """
    A transition where the current and incoming slide are placed side-by-side
    and gradually slide across the screen,
    with the current slide leaving and the incoming slide entering.
    """

    def initialize(
        self,
        from_widget: Widget,
        to_widget: Widget,
        direction: Direction,
    ) -> None:
        match direction:
            case Direction.Next:
                to_widget.styles.offset = ("100%", 0)
            case Direction.Previous:
                to_widget.styles.offset = ("-100%", 0)

    def progress(
        self,
        from_widget: Widget,
        to_widget: Widget,
        direction: Direction,
        progress: float,
    ) -> None:
        match direction:
            case Direction.Next:
                from_widget.styles.offset = (f"-{progress:.2f}%", 0)
                to_widget.styles.offset = (f"{100 - progress:.2f}%", 0)
            case Direction.Previous:
                from_widget.styles.offset = (f"{progress:.2f}%", 0)
                to_widget.styles.offset = (f"-{100 - progress:.2f}%", 0)

The transition effect is implemented using Textual CSS styles on the widgets that represent the "from" and "to" widgets.

Because the slide widgets are on different layers, they would normally both try to render in the "upper left corner" of the screen, and since the from slide is on the upper layer, it would be the one that actually gets rendered.

In Swipe.initialize, the to widget is moved to either the left or the right (depending on the transition direction) by 100%, i.e., it's own width. This puts the slides side-by-side, with the to slide fully off-screen.

As the transition progresses, the horizontal offsets of the two widgets are adjusted in lockstep so that they appear to move across the screen. Again, the direction of offset adjustment depends on the transition direction. The absolute value of the horizontal offsets always sums to 100%, which keeps the slides glued together as they move across the screen.

When progress=100 in the final state, the to widget will be at zero horizontal offset, and the from widget will be at plus or minus 100%, fully moved off-screen.

Contribute your transitions!

If you have developed a cool transition, consider contributing it to Spiel!