Button
The Button element with multiple variants, sizes, and states. Supports icons, loading states, and can be grouped together.
#Import
use gpui_kit::component::{
Sizable as _,
button::{Button, ButtonGroup, ButtonVariants as _},
};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Button, ButtonGroup, ButtonGroupVertical } from "button.slint";use iced::time::Instant;
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Subscription, Task, Theme};
use std::time::Duration;#Usage
The marked recipe below is a complete, Tested consumer recipe. The remaining examples are contextual fragments; keep the imports above when using variant or size builders.
use gpui_kit::IntoElement;
use gpui_kit::component::{
Sizable as _,
button::{Button, ButtonVariants as _},
};
pub fn primary_command() -> impl IntoElement {
Button::new("save").primary().small().label("Save changes")
}#Variants
use gpui_kit::component::button::ButtonVariants as _;
// Primary button
Button::new("btn-primary").primary().label("Primary")
// Secondary button (default)
Button::new("btn-secondary").label("Secondary")
// Danger button
Button::new("btn-danger").danger().label("Delete")
// Warning button
Button::new("btn-warning").warning().label("Warning")
// Success button
Button::new("btn-success").success().label("Success")
// Info button
Button::new("btn-info").info().label("Info")
// Ghost button
Button::new("btn-ghost").ghost().label("Ghost")
// Link button
Button::new("btn-link").link().label("Link")
// Text button
Button::new("btn-text").text().label("Text")HorizontalLayout {
alignment: center;
GridLayout {
horizontal-stretch: 0;
spacing: 8px;
Button {
horizontal-stretch: 0;
text: "Default";
}
Button {
horizontal-stretch: 0;
text: "Primary";
variant: ButtonVariant.primary;
}
Button {
horizontal-stretch: 0;
text: "Secondary";
variant: ButtonVariant.secondary;
}
Button {
horizontal-stretch: 0;
text: "Danger";
variant: ButtonVariant.danger;
}
Button {
horizontal-stretch: 0;
text: "Warning";
variant: ButtonVariant.warning;
}
Button {
horizontal-stretch: 0;
text: "Success";
variant: ButtonVariant.success;
}
Button {
horizontal-stretch: 0;
text: "Info";
variant: ButtonVariant.info;
}
}
}
HorizontalLayout {
alignment: center;
GridLayout {
horizontal-stretch: 0;
spacing: 8px;
Button {
horizontal-stretch: 0;
text: "Ghost";
variant: ButtonVariant.ghost;
}
Button {
horizontal-stretch: 0;
text: "Link";
variant: ButtonVariant.link;
}
Button {
horizontal-stretch: 0;
text: "Text";
variant: ButtonVariant.text;
}
}
}let variants = column![
row![
control("Default"),
control("Primary").variant(Variant::Primary),
control("Secondary").variant(Variant::Secondary),
control("Danger").variant(Variant::Danger),
control("Warning").variant(Variant::Warning),
control("Success").variant(Variant::Success),
control("Info").variant(Variant::Info),
]
.spacing(8.0)
.align_y(Alignment::Center),
row![
control("Ghost").variant(Variant::Ghost),
control("Link").variant(Variant::Link),
control("Text").variant(Variant::Text),
]
.spacing(8.0)
.align_y(Alignment::Center),
]
.spacing(8.0);#Outline
let outline = row![
control("Primary").variant(Variant::Primary).outline(),
control("Secondary").variant(Variant::Secondary).outline(),
control("Danger").variant(Variant::Danger).outline(),
control("Warning").variant(Variant::Warning).outline(),
control("Success").variant(Variant::Success).outline(),
control("Info").variant(Variant::Info).outline(),
]
.spacing(8.0)
.align_y(Alignment::Center);Outline style is not a variant itself, but can be combined with other variants.
use gpui_kit::component::button::ButtonVariants as _;
Button::new("btn").primary().outline().label("Primary Outline")
Button::new("btn").danger().outline().label("Danger Outline")HorizontalLayout {
alignment: center;
GridLayout {
horizontal-stretch: 0;
spacing: 8px;
Button {
horizontal-stretch: 0;
text: "Primary";
variant: ButtonVariant.primary;
outline: true;
}
Button {
horizontal-stretch: 0;
text: "Secondary";
variant: ButtonVariant.secondary;
outline: true;
}
Button {
horizontal-stretch: 0;
text: "Danger";
variant: ButtonVariant.danger;
outline: true;
}
Button {
horizontal-stretch: 0;
text: "Warning";
variant: ButtonVariant.warning;
outline: true;
}
Button {
horizontal-stretch: 0;
text: "Success";
variant: ButtonVariant.success;
outline: true;
}
Button {
horizontal-stretch: 0;
text: "Info";
variant: ButtonVariant.info;
outline: true;
}
}
}#Sizes
// 20, 24, 32 and 40 pixels tall, centred on one another so the row reads as a
// ladder rather than as four controls on a baseline.
let sizes = row![
control("Extra small").size(ControlSize::XSmall),
control("Small").size(ControlSize::Small),
control("Medium").size(ControlSize::Medium),
control("Large").size(ControlSize::Large),
]
.spacing(8.0)
.align_y(Alignment::Center);The compact method reduces the padding of the button for a more condensed appearance.
// Compact (reduced padding)
Button::new("btn")
.label("Compact")
.compact()HorizontalLayout {
alignment: center;
GridLayout {
horizontal-stretch: 0;
spacing: 8px;
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Button {
text: "Extra small";
size: ControlSize.xsmall;
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Button {
text: "Small";
size: ControlSize.small;
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Button {
text: "Medium";
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Button {
text: "Large";
size: ControlSize.large;
}
}
}
}#Icons and states
let icons_and_states = row![
control("Search").icon(glyph(SEARCH, Variant::Default, false)),
// Slint puts this one on the trailing edge; see the module note.
control("Options").icon(glyph(CHEVRON_DOWN, Variant::Default, false)),
icon_control("Settings", SETTINGS, Variant::Ghost, false),
icon_control("Add", PLUS, Variant::Primary, false),
control("Saving").loading(true),
control("Selected").selected(true),
// No message, so iced draws it disabled. This is the only control in the
// gallery that is inert on purpose.
Button::new("Disabled"),
]
.spacing(8.0)
.align_y(Alignment::Center);The icon method supports multiple types, allowing you to use different visual indicators:
- Icon / IconName - Static icons for actions and visual cues
- Spinner - Animated loading indicator for async operations
- ProgressCircle - Circular progress indicator showing completion percentage
All icon types automatically adapt to the button’s size and can be customized with colors and other properties.
#Icon Types
use gpui_kit::component::{Icon, IconName};
// Using IconName (simplest)
Button::new("btn")
.icon(IconName::Check)
.label("Confirm")
// Using Icon with custom size
Button::new("btn")
.icon(Icon::new(IconName::Heart))
.label("Like")
// Icon only (no label)
Button::new("btn")
.icon(IconName::Search)HorizontalLayout {
alignment: center;
GridLayout {
horizontal-stretch: 0;
spacing: 8px;
Button {
horizontal-stretch: 0;
text: "Search";
icon: "search";
}
Button {
horizontal-stretch: 0;
text: "Options";
suffix-icon: "chevron-down";
}
Button {
horizontal-stretch: 0;
icon: "settings";
variant: ButtonVariant.ghost;
}
Button {
horizontal-stretch: 0;
icon: "plus";
variant: ButtonVariant.primary;
}
Button {
horizontal-stretch: 0;
text: "Saving";
loading: true;
}
Button {
horizontal-stretch: 0;
text: "Selected";
selected: true;
}
Button {
horizontal-stretch: 0;
text: "Disabled";
enabled: false;
}
}
}#Spinner Icon
Use a Spinner to indicate loading or processing state:
use gpui_kit::component::{ActiveTheme as _, spinner::Spinner};
// Basic spinner
Button::new("btn")
.icon(Spinner::new())
.label("Loading...")
// Spinner with custom color
Button::new("btn")
.icon(Spinner::new().color(cx.theme().blue))
.label("Processing")
// Spinner with icon
Button::new("btn")
.icon(Spinner::new().icon(IconName::LoaderCircle))
.label("Syncing")#ProgressCircle Icon
Use a ProgressCircle to show progress percentage:
use gpui_kit::component::{
ActiveTheme as _, Sizable as _,
button::ButtonVariants as _,
progress::ProgressCircle,
};
// Basic progress circle
Button::new("btn")
.icon(ProgressCircle::new("install-progress").value(45.0))
.label("Installing...")
// Progress circle with custom color
Button::new("btn")
.primary()
.icon(
ProgressCircle::new("download-progress")
.value(75.0)
.color(cx.theme().primary_foreground)
)
.label("Downloading")
// Different sizes
Button::new("btn")
.small()
.icon(ProgressCircle::new("progress-1").value(60.0))
.label("Installing...")
Button::new("btn")
.large()
.icon(ProgressCircle::new("progress-2").value(80.0))
.label("Installing...")#Dynamic Icon Updates
Icons can be updated dynamically based on component state:
struct InstallButton {
progress: f32,
is_installing: bool,
}
impl InstallButton {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let button = Button::new("install-btn")
.label(if self.is_installing {
"Installing..."
} else {
"Install"
});
if self.is_installing {
button.icon(
ProgressCircle::new("install-progress")
.value(self.progress)
)
} else {
button.icon(IconName::Download)
}
}
}#Loading State with Icons
// A button that actually waits: the press starts the sync and the frame clock
// ends it, so the control is seen idle, working and done rather than only
// working. iced's button draws its loading form as the disabled treatment and
// does not swap the icon itself, so the demo supplies the mark the reference
// swaps in — a progress arc in the icon slot, advanced by the same clock.
let loading_state = row![
sync_control(app),
text(if app.syncing {
"Syncing…"
} else if app.pressed == Some("Sync done") {
"Synced"
} else {
"Idle"
})
.size(14.0),
]
.spacing(12.0)
.align_y(Alignment::Center);When a button is in loading state, it automatically handles icon transitions:
// If icon is already a Spinner or ProgressCircle, it will be shown during loading
Button::new("btn")
.icon(Spinner::new())
.label("Processing")
.loading(true) // Spinner will continue to show
// If icon is a regular Icon, it will be replaced with a Spinner during loading
Button::new("btn")
.icon(IconName::Save)
.label("Saving")
.loading(true) // Icon will be replaced with Spinner#Button group
// Touching, not joined: each button keeps its own radius, where Slint's
// `group-start`/`group-end` square the shared corners.
let groups = row![
row![
control("One").outline(),
control("Two").outline(),
control("Three").outline(),
]
.spacing(0.0)
.align_y(Alignment::Center),
column![
control("One").outline(),
control("Two").outline(),
control("Three").outline(),
]
.spacing(0.0),
]
.spacing(16.0)
.align_y(Alignment::Center);#Progress
let progress = row![
control("Installing").variant(Variant::Primary).loading(true),
control("Installing").loading(true),
]
.spacing(8.0)
.align_y(Alignment::Center);#Dropdown
let dropdown = row![
control("Primary")
.variant(Variant::Primary)
.icon(glyph(CHEVRON_DOWN, Variant::Primary, false)),
control("Default").icon(glyph(CHEVRON_DOWN, Variant::Default, false)),
control("Ghost")
.variant(Variant::Ghost)
.icon(glyph(CHEVRON_DOWN, Variant::Ghost, false)),
]
.spacing(8.0)
.align_y(Alignment::Center);#Custom size
let custom_size = row![icon_control("Favourite", HEART, Variant::Ghost, false)
.size(ControlSize::XSmall)]
.align_y(Alignment::Center);#Custom color
Button::new("my-button")
.label("Click me")
.on_click(|_, _, _| {
println!("Button clicked!");
})HorizontalLayout {
alignment: center;
VerticalLayout {
horizontal-stretch: 0;
spacing: 16px;
alignment: center;
ButtonGroup {
Button {
text: "One";
outline: true;
group-end: true;
}
Button {
text: "Two";
outline: true;
group-start: true;
group-end: true;
}
Button {
text: "Three";
outline: true;
group-start: true;
}
}
ButtonGroupVertical {
Button {
text: "One";
outline: true;
group-end: true;
group-vertical: true;
}
Button {
text: "Two";
outline: true;
group-start: true;
group-end: true;
group-vertical: true;
}
Button {
text: "Three";
outline: true;
group-start: true;
group-vertical: true;
}
}
}
}let custom_color = row![
control("Custom Button").variant(Variant::Success),
control("Outline Button").variant(Variant::Success).outline(),
]
.spacing(8.0)
.align_y(Alignment::Center);#Icon only
let icon_only = row![
icon_control("Search", SEARCH, Variant::Primary, false),
icon_control("Info", INFO, Variant::Default, false),
icon_control("Close", X, Variant::Danger, false),
icon_control("Search outlined", SEARCH, Variant::Default, true),
icon_control("Back", ARROW_LEFT, Variant::Ghost, false),
]
.spacing(8.0)
.align_y(Alignment::Center);The .dropdown_caret method can allows adding a dropdown caret icon to end of the button.
Button::new("btn")
.label("Options")
.dropdown_caret(true)HorizontalLayout {
alignment: center;
spacing: 8px;
Button {
icon: "search";
variant: ButtonVariant.primary;
}
Button {
icon: "info";
}
Button {
icon: "x";
variant: ButtonVariant.danger;
}
Button {
icon: "search";
outline: true;
}
Button {
icon: "arrow-left";
variant: ButtonVariant.ghost;
}
}#Sizeable
The Button supports the Sizable trait for different sizes.
use gpui_kit::component::Sizable as _;
Button::new("btn").xsmall().label("Extra Small")
Button::new("btn").small().label("Small")
Button::new("btn").label("Medium") // default
Button::new("btn").large().label("Large")HorizontalLayout {
alignment: center;
Button {
icon: "heart";
variant: ButtonVariant.ghost;
size: ControlSize.xsmall;
}
}#Button States
There have disabled, loading, selected state for buttons to indicate different statuses.
use gpui_kit::component::{Disableable as _, Selectable as _};
// Disabled
Button::new("btn")
.label("Disabled")
.disabled(true)
// Loading
Button::new("btn")
.label("Loading")
.loading(true)
// Selected
Button::new("btn")
.label("Selected")
.selected(true)#With Tooltip
Button::new("btn")
.label("Hover me")
.tooltip("This is a helpful tooltip")
.tooltip_placement(Placement::Bottom)Use .tooltip_placement(...) to prefer a side for either .tooltip(...) or
.tooltip_with_action(...). The tooltip still flips when that side does not fit.
Omit placement to keep automatic positioning.
#Custom Children
Button::new("btn")
.child(
h_flex()
.items_center()
.gap_2()
.child("Custom Content")
.child(IconName::ChevronDown)
.child(IconName::Eye)
)#Button Group
ButtonGroup::new("btn-group")
.child(Button::new("btn1").label("One"))
.child(Button::new("btn2").label("Two"))
.child(Button::new("btn3").label("Three"))#Toggle Button Group
use gpui_kit::component::Selectable as _;
ButtonGroup::new("toggle-group")
.multiple(true) // Allow multiple selections
.child(Button::new("btn1").label("Option 1").selected(true))
.child(Button::new("btn2").label("Option 2"))
.child(Button::new("btn3").label("Option 3"))
.on_click(|selected_indices, _, _| {
println!("Selected: {:?}", selected_indices);
})#Custom Variant
use gpui_kit::component::{
ActiveTheme as _, Colorize as _,
button::{ButtonCustomVariant, ButtonVariants as _},
};
let custom = ButtonCustomVariant::new(cx)
.color(cx.theme().magenta)
.foreground(cx.theme().primary_foreground)
.hover(cx.theme().magenta.opacity(0.1))
.active(cx.theme().magenta);
Button::new("custom-btn")
.custom(custom)
.label("Custom Button")HorizontalLayout {
alignment: center;
spacing: 8px;
Button {
text: "Custom Button";
variant: ButtonVariant.success;
}
Button {
text: "Outline Button";
variant: ButtonVariant.success;
outline: true;
}
}#API Reference
#Button
A push button. Mirrors GPUI Component's `Button`: variants, outline, sizes, leading and trailing icons, a loading spinner, and a selected state.
| Name | Kind | Type | Default | Description |
|---|---|---|---|---|
text | in | string | ||
icon | in | string | ||
suffix-icon | in | string | ||
variant | in | ButtonVariant | ButtonVariant.default | |
size | in | ControlSize | ControlSize.medium | |
outline | in | bool | ||
enabled | in | bool | true | |
group-start | in | bool | Square the leading or trailing side so adjacent buttons in a group share one edge. | |
group-end | in | bool | ||
group-vertical | in | bool | ||
loading | in | bool | ||
selected | in | bool | ||
has-hover | out | bool | touch.has-hover | |
pressed | out | bool | touch.pressed | |
clicked | callback | () |
#ButtonGroup
Joined buttons that share one outline. Set `group-start` and `group-end` on each child so only the outer corners stay rounded.