Progress

Progress components visually represent the completion percentage of a task. The library provides two variants:

Install

Both components feature smooth transition animations when the value changes, a loading (indeterminate) animation mode, customizable colors, and automatic styling that adapts to the current theme.

#Usage

#Progress

struct FileUpload {
    uploaded: u64,
    total: u64,
}

impl FileUpload {
    fn progress(&self) -> f32 {
        if self.total == 0 { return 0.0; }
        (self.uploaded as f32 / self.total as f32) * 100.0
    }

    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(
                h_flex()
                    .justify_between()
                    .child("Uploading...")
                    .child(format!("{:.0}%", self.progress())),
            )
            .child(Progress::new("upload").value(self.progress()))
    }
}
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Progress, ProgressCircle } from "progress.slint";

#Values

struct AppInit {
    loading: bool,
    progress: f32,
}

impl Render for AppInit {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_3()
            .child(
                h_flex()
                    .gap_2()
                    .items_center()
                    .child(
                        ProgressCircle::new("init-circle")
                            .loading(self.loading)
                            .value(self.progress)
                            .size_4(),
                    )
                    .child(if self.loading { "Initializing..." } else { "Ready" }),
            )
            .child(
                Progress::new("init-bar")
                    .loading(self.loading)
                    .value(self.progress),
            )
    }
}
Progress {
    value: 0;
}
Progress {
    value: 25;
}
Progress {
    value: 75;
}
Progress {
    value: 100;
}
let values = column![
    Progress::new(0.0),
    Progress::new(0.25),
    Progress::new(0.5),
    Progress::new(0.75),
    Progress::new(1.0),
]
.spacing(12);

#Sizes and status colors

struct Install {
    step: usize,       // current package index
    total: usize,      // total packages
    step_progress: f32,
}

impl Install {
    fn overall(&self) -> f32 {
        if self.total == 0 { return 0.0; }
        (self.step as f32 + self.step_progress / 100.0) / self.total as f32 * 100.0
    }

    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(
                h_flex()
                    .justify_between()
                    .child(format!("Package {}/{}", self.step + 1, self.total))
                    .child(format!("{:.0}%", self.overall())),
            )
            .child(Progress::new("overall").value(self.overall()))
            .child(
                h_flex()
                    .gap_2()
                    .items_center()
                    .child(Progress::new("package").value(self.step_progress).small())
                    .child("Current package"),
            )
    }
}
GridLayout {
    spacing: 24px;
    VerticalLayout {
        horizontal-stretch: 0;
        spacing: 12px;
        alignment: center;
        Progress {
            value: 50;
            size: ControlSize.xsmall;
        }
        Progress {
            value: 50;
            size: ControlSize.small;
        }
        Progress {
            value: 50;
        }
        Progress {
            value: 50;
            size: ControlSize.large;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        spacing: 12px;
        alignment: center;
        Progress {
            value: 30;
            status: Status.info;
        }
        Progress {
            value: 60;
            status: Status.success;
        }
        Progress {
            value: 45;
            status: Status.warning;
        }
        Progress {
            value: 80;
            status: Status.danger;
        }
    }
}
let sizes = column![
    text("The four heights: 4, 6, 8 and 10 pixels.").size(14.0),
    Progress::new(value).size(ControlSize::XSmall),
    Progress::new(value).size(ControlSize::Small),
    Progress::new(value),
    Progress::new(value).size(ControlSize::Large),
    text("Two of the status colours.").size(14.0),
    Progress::new(value).status(Status::Success),
    Progress::new(value).status(Status::Danger),
]
.spacing(12);

#Progress circle

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
let circles = column![
    text("The circle, at 8, 12, 16 and 20 across.").size(14.0),
    row![
        ProgressCircle::new(value).size(ControlSize::XSmall),
        ProgressCircle::new(value).size(ControlSize::Small),
        ProgressCircle::new(value),
        ProgressCircle::new(value).size(ControlSize::Large),
        ProgressCircle::new(value).diameter(48.0),
        ProgressCircle::new(value).loading(app.loading),
        ProgressCircle::new(value).status(Status::Warning),
    ]
    .spacing(12)
    .align_y(Alignment::Center),
]
.spacing(12);

The live preview above is this sample.

GPUINo GPUI version of this example yet.
GridLayout {
    spacing: 16px;
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: root.value;
            loading: root.loading;
            size: ControlSize.xsmall;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: root.value;
            loading: root.loading;
            size: ControlSize.small;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: root.value;
            loading: root.loading;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: root.value;
            loading: root.loading;
            size: ControlSize.large;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: 75;
            status: Status.success;
            size: ControlSize.large;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: 40;
            status: Status.warning;
            size: ControlSize.large;
        }
    }
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            loading: true;
            size: ControlSize.large;
        }
    }
    ProgressCircle {
        horizontal-stretch: 0;
        value: root.value;
        loading: root.loading;
        width: 80px;
        VerticalLayout {
            alignment: center;
            spacing: 2px;
            Text {
                text: root.loading ? "…" : "\{round(root.value)}%";
                color: Theme.primary;
                font-size: Theme.font-size-sm;
                font-weight: 600;
                horizontal-alignment: center;
            }
            Text {
                text: "Loading";
                color: Theme.muted-foreground;
                font-size: Theme.font-size-xs;
                horizontal-alignment: center;
            }
        }
    }
}
GridLayout {
    spacing: 8px;
    VerticalLayout {
        horizontal-stretch: 0;
        alignment: center;
        ProgressCircle {
            value: root.value;
            loading: root.loading;
        }
    }
    Text {
        horizontal-stretch: 0;
        text: root.loading ? "Initializing…" : "Downloading…";
        color: Theme.foreground;
        font-size: Theme.font-size-sm;
        vertical-alignment: center;
    }
}

#Progress

use gpui_kit::component::progress::Progress;
// One row, packed left. These are small controls and always fit, so the
// wrap grid only ever broke them onto separate lines.
HorizontalLayout {
    spacing: 8px;
    alignment: start;
    Button {
        horizontal-stretch: 0;
        text: "Loading";
        selected: root.loading;
        clicked => {
            root.loading = !root.loading;
        }
    }
    Button {
        horizontal-stretch: 0;
        icon: "minus";
        enabled: root.value > 0;
        clicked => {
            root.value = max(0, root.value - 10);
        }
    }
    Button {
        horizontal-stretch: 0;
        icon: "plus";
        enabled: root.value < 100;
        clicked => {
            root.value = min(100, root.value + 10);
        }
    }
}
VerticalLayout {
    spacing: 8px;
    GridLayout {
        Text {
            text: root.loading ? "Preparing upload…" : "Uploading report.pdf";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
            horizontal-stretch: 1;
        }
        Text {
            horizontal-stretch: 0;
            text: root.loading ? "" : "\{round(root.value)}%";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-sm;
        }
    }
    Progress {
        value: root.value;
        loading: root.loading;
    }
}
// The buttons stand in for the subscription an application would run: each
// press moves the value the bar and the loading form read, and `Loading` is
// the one that changes the form rather than the value.
let progress = column![
    row![
        Button::new("Advance")
            .variant(Variant::Primary)
            .on_press(Message::Step),
        Button::new("Reset").on_press(Message::Reset),
        Button::new("Loading")
            .selected(app.loading)
            .on_press(Message::ToggleLoading),
        caption(value, app.loading),
    ]
    .spacing(12)
    .align_y(Alignment::Center),
    Progress::new(value),
    text(if app.loading {
        "Loading: the value is the phase of the band."
    } else {
        "Determinate: the value is the filled width."
    })
    .size(14.0),
    Progress::new(value).loading(app.loading),
]
.spacing(12);

#Usage

Progress::new("my-progress")
    .value(75.0) // 75% complete
SlintNo Slint version of this example yet.

#Different Progress Values

Progress::new("progress-0").value(0.0)
Progress::new("progress-25").value(25.0)
Progress::new("progress-75").value(75.0)
Progress::new("progress-100").value(100.0)
SlintNo Slint version of this example yet.

#Loading State

Use .loading(true) to show an indeterminate animation when the actual progress is unknown. The value is ignored while loading is active.

// Indeterminate loading animation
Progress::new("loading").loading(true)

// Toggle between loading and determinate
Progress::new("my-progress")
    .loading(self.is_loading)
    .value(self.progress)
SlintNo Slint version of this example yet.

#Sizes

Progress implements the Sizable trait:

Progress::new("xs").value(50.0).xsmall()  // 4px height
Progress::new("sm").value(50.0).small()   // 6px height
Progress::new("md").value(50.0)           // 8px height (default)
Progress::new("lg").value(50.0).large()   // 10px height
SlintNo Slint version of this example yet.

#Custom Style

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The page's own sample — a 16-pixel bar with a 2-pixel corner and a 2-pixel
// edge, none of which the size ladder above can express. Its `green_light`
// fill and `green` edge are the success token at half strength and whole.
let custom_style = column![
    Progress::new(value)
        .height(16.0)
        .rounded(2.0)
        .color(theme::alpha(app.tokens.success, 0.5))
        .border(2.0)
        .border_color(app.tokens.success),
]
.spacing(12);

The component implements the Styled trait, allowing custom height, border radius, color, and border:

Progress::new("custom")
    .value(32.0)
    .h(px(16.))
    .rounded(px(2.))
    .color(cx.theme().green_light)
    .border_2()
    .border_color(cx.theme().green)
SlintNo Slint version of this example yet.

#Dynamic Progress Updates

struct MyView {
    value: f32,
    is_loading: bool,
}

impl Render for MyView {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_3()
            .child(
                h_flex()
                    .gap_2()
                    .child(
                        Button::new("toggle-loading")
                            .label("Loading")
                            .selected(self.is_loading)
                            .on_click(cx.listener(|this, _, _, cx| {
                                this.is_loading = !this.is_loading;
                                cx.notify();
                            })),
                    )
                    .child(Button::new("inc").icon(IconName::Plus).on_click(
                        cx.listener(|this, _, _, _| {
                            this.value = (this.value + 10.).min(100.);
                        }),
                    )),
            )
            .child(
                Progress::new("progress")
                    .value(self.value)
                    .loading(self.is_loading),
            )
    }
}
SlintNo Slint version of this example yet.

#API Reference

MethodTypeDescription
new(id)ElementIdCreate a new progress bar
value(v)f32Set progress value (0–100), clamped automatically
loading(v)boolEnable indeterminate loading animation; ignores value when true
color(c)impl Into<Hsla>Override the fill color (defaults to theme.progress_bar)
xsmall() / small() / large()—Set predefined height via Sizable
Styled trait methods—Custom height, border radius, border, etc.

#Progress

A linear progress bar. Mirrors GPUI Component's `Progress`: a 0–100 value with an eased transition, an indeterminate `loading` animation, sizes from 4px to 10px tall, and a fill color that follows `status` or an explicit `color`.

NameKindTypeDefault
valueinfloat
loadinginbool
sizeinControlSizeControlSize.medium
statusinStatusStatus.default
colorincolorstatus == Status.default ? Theme.primary : Theme.status-color(status)

#ProgressCircle

A circular progress indicator that displays progress as an arc. Ideal for compact spaces, inline labels, or as a download/upload indicator.

use gpui_kit::component::progress::ProgressCircle;
SlintNo Slint version of this example yet.

#Usage

ProgressCircle::new("circle").value(50.0)
SlintNo Slint version of this example yet.

#Loading State

// Indeterminate rotating arc animation
ProgressCircle::new("loading").loading(true)

// Toggle between loading and determinate
ProgressCircle::new("circle")
    .loading(self.is_loading)
    .value(self.progress)
SlintNo Slint version of this example yet.

#Sizes

ProgressCircle implements the Sizable trait. Named sizes map to fixed pixel dimensions; use .size(px(n)) for custom sizes:

ProgressCircle::new("xs").value(50.0).xsmall()    // size_2
ProgressCircle::new("sm").value(50.0).small()     // size_3
ProgressCircle::new("md").value(50.0)             // size_4 (default)
ProgressCircle::new("lg").value(50.0).large()     // size_5
ProgressCircle::new("xl").value(50.0).size_20()   // 80px
SlintNo Slint version of this example yet.

#Custom Color

ProgressCircle::new("green").value(75.0).color(cx.theme().green)
ProgressCircle::new("yellow").value(40.0).color(cx.theme().yellow)
ProgressCircle::new("primary").value(60.0).color(cx.theme().primary)
SlintNo Slint version of this example yet.

#With Inner Content

ProgressCircle implements ParentElement, so you can place content inside the circle:

ProgressCircle::new("circle-with-label")
    .value(self.value)
    .size_20()
    .child(
        v_flex()
            .size_full()
            .items_center()
            .justify_center()
            .gap_1()
            .child(
                div()
                    .child(format!("{}%", self.value as i32))
                    .text_color(cx.theme().progress_bar),
            )
            .child(div().child("Loading").text_xs()),
    )
SlintNo Slint version of this example yet.

#Inline with Label

h_flex()
    .gap_2()
    .items_center()
    .child(
        ProgressCircle::new("download")
            .color(cx.theme().primary)
            .value(self.progress)
            .size_4(),
    )
    .child("Downloading...")
SlintNo Slint version of this example yet.

#API Reference

MethodTypeDescription
new(id)ElementIdCreate a new circular progress indicator
value(v)f32Set progress value (0–100), clamped automatically
loading(v)boolEnable indeterminate loading animation; ignores value when true
color(c)impl Into<Hsla>Override the arc color (defaults to theme.progress_bar)
xsmall() / small() / large()—Set predefined size via Sizable
size(px(n))PixelsSet custom size
ParentElement—Place content inside the circle

#ProgressCircle

A circular progress indicator. Mirrors GPUI Component's `ProgressCircle`: a 0–100 arc drawn clockwise from the top over a tinted track, an indeterminate `loading` arc, sizes from 8px to 20px (set `width` for a custom diameter), `status`/`color`, and children centered inside the circle.

NameKindTypeDefault
valueinfloat
loadinginbool
sizeinControlSizeControlSize.medium
statusinStatusStatus.default
colorincolorstatus == Status.default ? Theme.primary : Theme.status-color(status)