Skip to content

ProgressBar

A widget that displays progress on a time-consuming task.

  • Focusable
  • Container

Examples

Progress Bar in Isolation

The example below shows a progress bar in isolation. It shows the progress bar in:

  • its indeterminate state, when the total progress hasn't been set yet;
  • the middle of the progress; and
  • the completed state.

IndeterminateProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:--  s Start ^p palette

IndeterminateProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07  s Start ^p palette

IndeterminateProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:--  s Start ^p palette

from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class IndeterminateProgressBar(App[None]):
    BINDINGS = [("s", "start", "Start")]

    progress_timer: Timer
    """Timer to simulate progress happening."""

    def compose(self) -> ComposeResult:
        with Center():
            with Middle():
                yield ProgressBar()
        yield Footer()

    def on_mount(self) -> None:
        """Set up a timer to simulate progess happening."""
        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

    def make_progress(self) -> None:
        """Called automatically to advance the progress bar."""
        self.query_one(ProgressBar).advance(1)

    def action_start(self) -> None:
        """Start the progress tracking."""
        self.query_one(ProgressBar).update(total=100)
        self.progress_timer.resume()


if __name__ == "__main__":
    IndeterminateProgressBar().run()

Complete App Example

The example below shows a simple app with a progress bar that is keeping track of a fictitious funding level for an organisation.

Funding tracking Funding tracking Funding: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━0% ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ $$$ Donate  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

Funding tracking Funding tracking Funding: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━35% ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ $$$ Donate  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Donation for $15 received! Donation for $20 received!

Funding tracking Funding tracking Funding: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100% ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ $$$ Donate  ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Donation for $15 received! Donation for $20 received! Donation for $65 received!

from textual.app import App, ComposeResult
from textual.containers import Center, VerticalScroll
from textual.widgets import Button, Header, Input, Label, ProgressBar


class FundingProgressApp(App[None]):
    CSS_PATH = "progress_bar.tcss"

    TITLE = "Funding tracking"

    def compose(self) -> ComposeResult:
        yield Header()
        with Center():
            yield Label("Funding: ")
            yield ProgressBar(total=100, show_eta=False)  # (1)!
        with Center():
            yield Input(placeholder="$$$")
            yield Button("Donate")

        yield VerticalScroll(id="history")

    def on_button_pressed(self) -> None:
        self.add_donation()

    def on_input_submitted(self) -> None:
        self.add_donation()

    def add_donation(self) -> None:
        text_value = self.query_one(Input).value
        try:
            value = int(text_value)
        except ValueError:
            return
        self.query_one(ProgressBar).advance(value)
        self.query_one(VerticalScroll).mount(Label(f"Donation for ${value} received!"))
        self.query_one(Input).value = ""


if __name__ == "__main__":
    FundingProgressApp().run()
  1. We create a progress bar with a total of 100 steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task.
Container {
    overflow: hidden hidden;
    height: auto;
}

Center {
    margin-top: 1;
    margin-bottom: 1;
    layout: horizontal;
}

ProgressBar {
    padding-left: 3;
}

Input {
    width: 16;
}

VerticalScroll {
    height: auto;
}

Gradient Bars

Progress bars support an optional gradient parameter, which renders a smooth gradient rather than a solid bar. To use a gradient, create and set a Gradient object on the ProgressBar widget.

Note

Setting a gradient will override styles set in CSS.

Here's an example:

ProgressApp ━━━━━━━━━70%--:--:--

from textual.app import App, ComposeResult
from textual.color import Gradient
from textual.containers import Center, Middle
from textual.widgets import ProgressBar


class ProgressApp(App[None]):
    """Progress bar with a rainbow gradient."""

    def compose(self) -> ComposeResult:
        gradient = Gradient.from_colors(
            "#881177",
            "#aa3355",
            "#cc6666",
            "#ee9944",
            "#eedd00",
            "#99dd55",
            "#44dd88",
            "#22ccbb",
            "#00bbcc",
            "#0099cc",
            "#3366bb",
            "#663399",
        )
        with Center():
            with Middle():
                yield ProgressBar(total=100, gradient=gradient)

    def on_mount(self) -> None:
        self.query_one(ProgressBar).update(progress=70)


if __name__ == "__main__":
    ProgressApp().run()

Custom Styling

This shows a progress bar with custom styling. Refer to the section below for more information.

StyledProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:--  s Start ^p palette

StyledProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07  s Start ^p palette

StyledProgressBar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:--  s Start ^p palette

from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class StyledProgressBar(App[None]):
    BINDINGS = [("s", "start", "Start")]
    CSS_PATH = "progress_bar_styled.tcss"

    progress_timer: Timer
    """Timer to simulate progress happening."""

    def compose(self) -> ComposeResult:
        with Center():
            with Middle():
                yield ProgressBar()
        yield Footer()

    def on_mount(self) -> None:
        """Set up a timer to simulate progress happening."""
        self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

    def make_progress(self) -> None:
        """Called automatically to advance the progress bar."""
        self.query_one(ProgressBar).advance(1)

    def action_start(self) -> None:
        """Start the progress tracking."""
        self.query_one(ProgressBar).update(total=100)
        self.progress_timer.resume()


if __name__ == "__main__":
    StyledProgressBar().run()
Bar > .bar--indeterminate {
    color: $primary;
    background: $secondary;
}

Bar > .bar--bar {
    color: $primary;
    background: $primary 30%;
}

Bar > .bar--complete {
    color: $error;
}

PercentageStatus {
    text-style: reverse;