Stepper

A step-by-step progress component that guides users through a series of steps or stages. Supports horizontal and vertical layouts, custom icons, and different sizes.

Install

#Import

use gpui_kit::component::stepper::{Stepper, StepperItem};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { StepperItem, Stepper } from "stepper.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Font, Length, Subscription, Task, Theme, font};

#Usage

#With icons

use gpui_kit::component::IconName;

Stepper::new("icon-stepper")
    .selected_index(0)
    .items([
        StepperItem::new()
            .icon(IconName::Calendar)
            .child("Order Details"),
        StepperItem::new()
            .icon(IconName::Inbox)
            .child("Shipping"),
        StepperItem::new()
            .icon(IconName::Frame)
            .child("Preview"),
        StepperItem::new()
            .icon(IconName::Info)
            .child("Finish"),
    ])
order := Stepper {
    selected-index <=> root.order-step;
    items: [
        { label: "Order Details", icon: "calendar" },
        { label: "Shipping", icon: "inbox" },
        { label: "Preview", icon: "layers" },
        { label: "Finish", icon: "info" },
    ];
}
// One row, not a wrap grid. The wrap scheme is for cards that reflow;
// here a label that stretches and two buttons that do not fit at any
// width, and wrapping them only ever split the buttons across rows.
HorizontalLayout {
    spacing: 8px;
    cross-axis-alignment: center;
    Text {
        horizontal-stretch: 1;
        text: "Step \{root.order-step + 1} of \{order.items.length}";
        color: Theme.muted-foreground;
        font-size: Theme.font-size-sm;
        vertical-alignment: center;
    }
    Button {
        text: "Previous";
        icon: "chevron-left";
        enabled: root.order-step > 0;
        clicked => {
            root.order-step -= 1;
        }
    }
    Button {
        text: root.order-step == order.items.length - 1 ? "Done" : "Next";
        suffix-icon: root.order-step == order.items.length - 1 ? "" : "chevron-right";
        variant: ButtonVariant.primary;
        enabled: root.order-step < order.items.length - 1;
        clicked => {
            root.order-step += 1;
        }
    }
}
// The order stepper and the row beneath it: the count, and the two buttons
// that move the selection. Each button asks `step_index` for its neighbour,
// and a button with no neighbour has no message, which is iced's disabled
// button.
let order = Stepper::new()
    .items(order_items(app.order_step))
    .selected_index(app.order_step)
    .on_change(|index| Message::Selected(Slot::Order, index));

let count = 4;
let previous = stepper::step_index(&[false; 4], app.order_step, StepAction::Decrement)
    .map(|_| Message::OrderStep(StepAction::Decrement));
let next = stepper::step_index(&[false; 4], app.order_step, StepAction::Increment)
    .map(|_| Message::OrderStep(StepAction::Increment));
let last = app.order_step + 1 == count;

let icons = column![
    order,
    row![
        text(format!("Step {} of {}", app.order_step + 1, count))
            .size(theme::FONT_SIZE_SM)
            .width(Length::Fill)
            .style(|theme: &Theme| text::Style {
                color: Some(Tokens::of(theme).muted_foreground),
            }),
        order_button("Previous", CHEVRON_LEFT, previous, Variant::Default),
        order_button(
            if last { "Done" } else { "Next" },
            CHEVRON_RIGHT,
            next,
            Variant::Primary,
        ),
    ]
    .spacing(8.0)
    .align_y(Alignment::Center),
]
.spacing(16.0);

#Text center

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Each item's indicator is centred over its label and description, and the
// connector crosses the item on both sides of it.
let center = Stepper::new()
    .items([
        StepperItem::new()
            .label("Step 1")
            .description("Desc for step 1."),
        StepperItem::new()
            .label("Step 2")
            .description("Desc for step 2."),
        StepperItem::new()
            .label("Step 3")
            .description("Desc for step 3."),
    ])
    .text_center(true)
    .selected_index(app.center_step)
    .on_change(|index| Message::Selected(Slot::Center, index));

The text_center method centers the text within each step item.

Stepper::new("center-stepper")
    .selected_index(0)
    .text_center(true)
    .items([
        StepperItem::new().child(
            v_flex()
                .items_center()
                .child("Step 1")
                .child("Desc for step 1."),
        ),
        StepperItem::new().child(
            v_flex()
                .items_center()
                .child("Step 2")
                .child("Desc for step 2."),
        ),
        StepperItem::new().child(
            v_flex()
                .items_center()
                .child("Step 3")
                .child("Desc for step 3."),
        ),
    ])
Stepper {
    selected-index <=> root.center-step;
    text-center: true;
    items: [
        { label: "Step 1", description: "Desc for step 1." },
        { label: "Step 2", description: "Desc for step 2." },
        { label: "Step 3", description: "Desc for step 3." },
    ];
}

#Vertical

Stepper::new("vertical-stepper")
    .vertical()
    .selected_index(2)
    .items_center()
    .items([
        StepperItem::new()
            .pb_8()
            .icon(IconName::Building2)
            .child(v_flex().child("Step 1").child("Description for step 1.")),
        StepperItem::new()
            .pb_8()
            .icon(IconName::Asterisk)
            .child(v_flex().child("Step 2").child("Description for step 2.")),
        StepperItem::new()
            .pb_8()
            .icon(IconName::Folder)
            .child(v_flex().child("Step 3").child("Description for step 3.")),
        StepperItem::new()
            .icon(IconName::CircleCheck)
            .child(v_flex().child("Step 4").child("Description for step 4.")),
    ])
horizontal-stretch: 1;
Stepper {
    vertical: true;
    selected-index <=> root.vertical-step;
    items: [
        { label: "Step 1", description: "Description for step 1.", icon: "house" },
        { label: "Step 2", description: "Description for step 2.", icon: "sparkles" },
        { label: "Step 3", description: "Description for step 3.", icon: "folder" },
        { label: "Step 4", description: "Description for step 4.", icon: "circle-check" },
    ];
}
// The indicators run down the leading edge with the labels and descriptions
// beside them, and the connector runs between the indicators.
let vertical = Stepper::vertical()
    .items(vertical_items(app.vertical_step))
    .selected_index(app.vertical_step)
    .on_change(|index| Message::Selected(Slot::Vertical, index));

#Sizes and disabled

use gpui_kit::component::{Sizable as _, Size};

Stepper::new("stepper")
    .xsmall()
    .items([...])

Stepper::new("stepper")
    .small()
    .items([...])

Stepper::new("stepper")
    .large()
    .items([...])
horizontal-stretch: 2;
Stepper {
    size: ControlSize.xsmall;
    selected-index <=> root.size-step;
    items: [
        { label: "Cart" },
        { label: "Address" },
        { label: "Payment" }
    ];
}
Stepper {
    size: ControlSize.small;
    selected-index <=> root.size-step;
    items: [
        { label: "Cart" },
        { label: "Address" },
        { label: "Payment" }
    ];
}
Stepper {
    size: ControlSize.large;
    selected-index <=> root.size-step;
    items: [
        { label: "Cart" },
        { label: "Address" },
        { label: "Payment" }
    ];
}
Stepper {
    items: [
        { label: "Available" },
        { label: "Locked", disabled: true },
        { label: "Available" }
    ];
}
Stepper {
    enabled: false;
    selected-index: 1;
    items: [
        { label: "Account" },
        { label: "Profile" },
        { label: "Review" }
    ];
}
// The three size steppers share one selection, the way Slint binds all three
// to `size-step`: clicking any of them moves the three together. Beside them
// a single disabled step, and a whole stepper disabled with `enabled: false`
// — which is the absence of a message here, not a faded subtree.
let sizes = column![
    Stepper::new()
        .size(ControlSize::XSmall)
        .items(size_items())
        .selected_index(app.size_step)
        .on_change(|index| Message::Selected(Slot::Size, index)),
    Stepper::new()
        .size(ControlSize::Small)
        .items(size_items())
        .selected_index(app.size_step)
        .on_change(|index| Message::Selected(Slot::Size, index)),
    Stepper::new()
        .size(ControlSize::Large)
        .items(size_items())
        .selected_index(app.size_step)
        .on_change(|index| Message::Selected(Slot::Size, index)),
    Stepper::new()
        .items([
            StepperItem::new().label("Available"),
            StepperItem::new().label("Locked").disabled(true),
            StepperItem::new().label("Available"),
        ])
        .selected_index(app.gap_step)
        .on_change(|index| Message::Selected(Slot::Gap, index)),
    Stepper::new()
        .items(size_items())
        .selected_index(1)
        .disabled(true),
    muted_line("The xsmall, small and large steppers share one selection; the locked step refuses a press, and the last stepper refuses every one."),
]
.spacing(theme::SPACING_LG);

#Basic Stepper

Use selected_index method to set current active step by index (0-based), default is 0.

Stepper::new("my-stepper")
    .selected_index(0)
    .items([
        StepperItem::new().child("Step 1"),
        StepperItem::new().child("Step 2"),
        StepperItem::new().child("Step 3"),
    ])
    .on_click(|step, _, _| {
        println!("Clicked step: {}", step);
    })
SlintNo Slint version of this example yet.

#Disabled State

Stepper::new("disabled-stepper")
    .disabled(true)
    .items([
        StepperItem::new().child("Step 1"),
        StepperItem::new().child("Step 2"),
    ])
SlintNo Slint version of this example yet.

#Handle Click Events

Stepper::new("my-stepper")
    .selected_index(current_step)
    .items([
        StepperItem::new().child("Step 1"),
        StepperItem::new().child("Step 2"),
        StepperItem::new().child("Step 3"),
    ])
    .on_click(cx.listener(|this, step, _, cx| {
        this.current_step = *step;
        cx.notify();
    }))
SlintNo Slint version of this example yet.

#Multi-step Form

Stepper::new("form-stepper")
    .w_full()
    .selected_index(form_step)
    .items([
        StepperItem::new()
            .icon(IconName::User)
            .child("Personal Info"),
        StepperItem::new()
            .icon(IconName::CreditCard)
            .child("Payment"),
        StepperItem::new()
            .icon(IconName::CircleCheck)
            .child("Confirmation"),
    ])
    .on_click(cx.listener(|this, step, _, cx| {
        this.form_step = *step;
        cx.notify();
    }))
SlintNo Slint version of this example yet.

#Disabled Individual Steps

Stepper::new("stepper")
    .selected_index(0)
    .items([
        StepperItem::new().child("Available"),
        StepperItem::new().disabled(true).child("Locked"),
        StepperItem::new().child("Available"),
    ])
SlintNo Slint version of this example yet.

#API Reference

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// One control built with every method the reference lists, and the step a
// programmatic Previous/Next takes: `stepper::step_index`, which is
// `number_input::step_value` plus the rule that a disabled step is passed
// over.
let api = column![
    Stepper::new()
        .items([
            StepperItem::new().label("Cart"),
            StepperItem::new().label("Address"),
            StepperItem::new().label("Payment"),
        ])
        .selected_index(app.api_step)
        .size(ControlSize::Medium)
        .text_center(false)
        .disabled(false)
        .width(Length::Fill)
        .on_change(|index| Message::Selected(Slot::Api, index)),
    muted_line("step_index(&[false, true, false], 0, Increment) passes the locked step and lands on Payment."),
]
.spacing(10.0);

#Sizing

Implements Sizable trait:

  • xsmall() - Extra small size
  • small() - Small size
  • medium() - Medium size (default)
  • large() - Large size

#StepperItem

NameKindTypeDefault
labelfieldstring
descriptionfieldstring
iconfieldstring
disabledfieldbool

#Stepper

A step-by-step progress indicator. Mirrors GPUI Component's `Stepper` and `StepperItem`: steps up to `selected-index` are checked and the connectors between passed steps turn primary; horizontal or `vertical` layout, `text-center`, sizes, optional icons and descriptions, and per-step or whole-stepper disabling. Clicking a step, or Left/Right (Up/Down when vertical) with keyboard focus, selects it and emits `clicked(index)`.

NameKindTypeDefault
itemsin[StepperItem]
selected-indexin-outint
verticalinbool
text-centerinbool
sizeinControlSizeControlSize.medium
enabledinbooltrue
clickedcallback(index: int)