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.
#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
// 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);
})#Disabled State
Stepper::new("disabled-stepper")
.disabled(true)
.items([
StepperItem::new().child("Step 1"),
StepperItem::new().child("Step 2"),
])#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();
}))#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();
}))#Disabled Individual Steps
Stepper::new("stepper")
.selected_index(0)
.items([
StepperItem::new().child("Available"),
StepperItem::new().disabled(true).child("Locked"),
StepperItem::new().child("Available"),
])#API Reference
// 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 sizesmall()- Small sizemedium()- Medium size (default)large()- Large size
#StepperItem
| Name | Kind | Type | Default |
|---|---|---|---|
label | field | string | |
description | field | string | |
icon | field | string | |
disabled | field | bool |
#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)`.
| Name | Kind | Type | Default |
|---|---|---|---|
items | in | [StepperItem] | |
selected-index | in-out | int | |
vertical | in | bool | |
text-center | in | bool | |
size | in | ControlSize | ControlSize.medium |
enabled | in | bool | true |
clicked | callback | (index: int) |