Button

The Button element with multiple variants, sizes, and states. Supports icons, loading states, and can be grouped together.

Install

#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")
}
SlintNo Slint version of this example yet.

#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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
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")
SlintNo Slint version of this example yet.

#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...")
SlintNo Slint version of this example yet.

#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)
        }
    }
}
SlintNo Slint version of this example yet.

#Loading State with Icons

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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
SlintNo Slint version of this example yet.

#Button group

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
let progress = row![
    control("Installing").variant(Variant::Primary).loading(true),
    control("Installing").loading(true),
]
.spacing(8.0)
.align_y(Alignment::Center);
GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
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)
SlintNo Slint version of this example yet.

#With Tooltip

Button::new("btn")
    .label("Hover me")
    .tooltip("This is a helpful tooltip")
    .tooltip_placement(Placement::Bottom)
SlintNo Slint version of this example yet.

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)
    )
SlintNo Slint version of this example yet.

#Button Group

ButtonGroup::new("btn-group")
    .child(Button::new("btn1").label("One"))
    .child(Button::new("btn2").label("Two"))
    .child(Button::new("btn3").label("Three"))
SlintNo Slint version of this example yet.

#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);
    })
SlintNo Slint version of this example yet.

#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.

NameKindTypeDefaultDescription
textinstring
iconinstring
suffix-iconinstring
variantinButtonVariantButtonVariant.default
sizeinControlSizeControlSize.medium
outlineinbool
enabledinbooltrue
group-startinboolSquare the leading or trailing side so adjacent buttons in a group share one edge.
group-endinbool
group-verticalinbool
loadinginbool
selectedinbool
has-hoveroutbooltouch.has-hover
pressedoutbooltouch.pressed
clickedcallback()

#ButtonGroup

Joined buttons that share one outline. Set `group-start` and `group-end` on each child so only the outer corners stay rounded.

#ButtonGroupVertical