Skip to content

Spinners

To show that something is still working, such as each step of a coding agent, animate a spinner with use_clock:

FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
FRAME_TIME = timedelta(milliseconds=80)


@component
def spinner(label: str) -> Text:
    elapsed = use_clock(FRAME_TIME).since_mount
    frame = FRAMES[elapsed // FRAME_TIME % len(FRAMES)]
    return Text(content=f"{frame} {label} ({elapsed // timedelta(seconds=1)}s)", style=text_color("amber", 400))


@component
def progress(done: tuple[str, ...], current: str) -> Div:
    return Div(
        style=col,
        children=[
            *(Text(content=f"✓ {step}", style=text_color("emerald", 500)) for step in done),
            # Keyed by the step, so each new step remounts the spinner and restarts its time.
            spinner(current).with_key(current),
        ],
    )

Spinner

✓ Read pyproject.toml                   
✓ Ran the tests                         
⠹ Fixing the failing test (4s)          

spinner reads the clock once per frame of the animation, and computes which frame to show from how long it has been mounted, so the animation keeps pace with real time however long the step takes (see Why compute from elapsed time?). The same reading gives the elapsed seconds beside the label.

Each frame re-runs spinner alone, since it's the only component that reads the clock. progress re-runs only when its steps change, and gives the spinner a new key for each step, which remounts it and starts its time again from zero.