Skip to content
PorabuildHeroGPUI component library
v0.14.0 ยท Apache-2.0

Buttons

Button

A pressable button with variants and states.

Rust
use herogpui::prelude::{Button, Size, Variant};

Button on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

`radius` sets the corners; `sx` squares the top-left corner, including while pressed. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![Button::new("button")
        .radius(px(8.))
        .sx(|el| el.rounded_tl(px(0.)))
        .label("Click me")
        .into_any_element()])

Variants

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(Variant::ALL
        .iter()
        .filter_map(|v| {
            let key = format!("btn-v-{v:?}");
            Some({
                Button::new(key)
                    .label(v.label())
                    .variant(*v)
            })
        }))

Sizes

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(Size::ALL
        .iter()
        .filter_map(|s| {
            let key = format!("btn-s-{s:?}");
            Some({
                Button::new(key)
                    .label(s.label())
                    .size(*s)
            })
        }))

With Icons

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("button-1")
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::GLOBE)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Search")
            .into_any_element(),
        Button::new("button-2")
            .variant(Variant::Secondary)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::PLUS)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Add Member")
            .into_any_element(),
        Button::new("button-3")
            .variant(Variant::Tertiary)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::MAIL)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Email")
            .into_any_element(),
        Button::new("button-4")
            .variant(Variant::Danger)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::TRASH)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Delete")
            .into_any_element(),
    ])

Icon Only

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("button-1")
            .is_icon_only(true)
            .variant(Variant::Tertiary)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::ELLIPSIS)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .into_any_element(),
        Button::new("button-2")
            .is_icon_only(true)
            .variant(Variant::Secondary)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::GEAR)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .into_any_element(),
        Button::new("button-3")
            .is_icon_only(true)
            .variant(Variant::Danger)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::TRASH)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .into_any_element(),
    ])

Loading

A static pending button: the spinner replaces the label's leading slot while `is_pending` holds, and the caller decides when the work is done. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Spinner, SpinnerSize, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let pending_foreground = cx.colors().accent.foreground;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![Button::new("button-loading")
        .is_pending(true)
        .content(move |state| {
            gpui::div()
                .flex()
                .items_center()
                .gap(px(8.))
                .when(state.is_pending, |content| {
                    content.child(
                        Spinner::new("button-loading-spinner")
                            .size(SpinnerSize::Sm)
                            .current_color(pending_foreground),
                    )
                })
                .child("Uploading...")
                .into_any_element()
        })
        .into_any_element()])

Loading State

The caller owns the pending window: pressing starts an upload and the state resets itself after two seconds. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Spinner, SpinnerSize, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let pending_foreground = cx.colors().accent.foreground;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![Button::new("button-loading-state")
        .is_pending(self.button_upload_pending)
        .content(move |state| {
            gpui::div()
                .flex()
                .items_center()
                .gap(px(8.))
                .when(state.is_pending, |content| {
                    content.child(
                        Spinner::new("button-loading-state-spinner")
                            .size(SpinnerSize::Sm)
                            .current_color(pending_foreground),
                    )
                })
                .child(if state.is_pending {
                    "Uploading..."
                } else {
                    "Upload File"
                })
                .into_any_element()
        })
        .on_press({
            let gallery = cx.entity().downgrade();
            move |_, window, cx: &mut gpui::App| {
                if let Some(gallery) = gallery.upgrade() {
                    gallery.update(cx, |gallery, cx| {
                        gallery.button_upload_pending = true;
                        cx.notify();
                    });
                }
                let gallery = gallery.clone();
                window
                    .spawn(cx, async move |cx| {
                        cx.background_executor()
                            .timer(std::time::Duration::from_millis(2000))
                            .await;
                        cx.update(|_, cx| {
                            if let Some(gallery) = gallery.upgrade() {
                                gallery.update(cx, |gallery, cx| {
                                    gallery.button_upload_pending = false;
                                    cx.notify();
                                });
                            }
                        })
                        .ok();
                    })
                    .detach();
            }
        })
        .into_any_element()])

Full Width

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w(px(400.))
    .gap(px(12.))
    .child(
        Button::new("button-full")
            .label("Primary Button")
            .full_width(true),
    )
    .child(
        Button::new("button-full-icon")
            .full_width(true)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::PLUS)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("With Icon"),
    )

Pixel Geometry

`width`, `min_width`, `height` and `padding_x` set the box in place of the size ladder. An explicit pixel `width` beats `full_width`, and the pressed skin keeps the fixed box. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![gpui::div()
        .flex()
        .items_start()
        .gap(px(12.))
        .child(
            Button::new("button-geom-sized")
                .label("200 x 56")
                .width(px(200.))
                .height(px(56.)),
        )
        .child(
            Button::new("button-geom-roomy")
                .label("Roomy")
                .padding_x(px(32.)),
        )
        .child(
            Button::new("button-geom-min")
                .label("Min 160")
                .min_width(px(160.)),
        )
        .into_any_element()])

Text Metrics

`text_size` and `font_weight` restyle the label in place of the size step's `text-sm font-medium`; a Tailwind step keeps its paired leading. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("button-metrics-large")
            .label("18px SemiBold")
            .text_size(px(18.))
            .font_weight(gpui::FontWeight::SEMIBOLD)
            .into_any_element(),
        Button::new("button-metrics-small")
            .label("12px medium")
            .text_size(px(12.))
            .into_any_element(),
    ])

Flex Fill

`grow` is v3's `flex-1` plus `min-w-0`: the growing buttons share the row's free width and compress below their content width when the row is tight, while the fixed one keeps its own. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![gpui::div()
        .flex()
        .w(px(400.))
        .gap(px(8.))
        .child(Button::new("button-grow-fill").label("Fill").grow(true))
        .child(Button::new("button-grow-share").label("Shares too").grow(true))
        .child(Button::new("button-grow-fixed").label("Fixed"))
        .into_any_element()])

Disabled State

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(Variant::ALL
        .iter()
        .filter_map(|v| {
            let key = format!("btn-d-{v:?}");
            Some({
                Button::new(key)
                    .label(v.label())
                    .variant(*v)
                    .is_disabled(true)
            })
        }))

Social Buttons

Full-width tertiary buttons behind a leading brand mark. The marks are trademarks, so this example shows the same layout with its own glyphs. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Size, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .w(px(280.))
    .gap(px(12.))
    .child(
        Button::new("button-1")
            .variant(Variant::Tertiary)
            .full_width(true)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::MAIL)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Sign in with Email"),
    )
    .child(
        Button::new("button-2")
            .variant(Variant::Tertiary)
            .full_width(true)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::KEY)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Sign in with a passkey"),
    )
    .child(
        Button::new("button-3")
            .variant(Variant::Tertiary)
            .full_width(true)
            .child(gpui::svg()
                       .size(px(16.))
                       .path(icons::GLOBE)
                       .text_color(cx.colors().foreground)
                       .into_any_element())
            .child("Single sign-on"),
    )

Adding custom variants

Fix a preset in place: a constructor layers its own radius and emphasis on top of the base, and callers keep the plain `Button` API. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children({
        let custom_button = |id: &'static str, label: &'static str| {
            Button::new(id)
                .label(label)
                .variant(Variant::Secondary)
                .size(Size::Lg)
                .sx(|el| el.rounded_full())
        };
        vec![
            custom_button("btn-custom-primary", "Custom Button")
                .into_any_element(),
            custom_button("btn-custom-secondary", "Another Preset")
                .into_any_element(),
        ]
    })

Press handler

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;

let clicks = self.button_clicks;

Button::new("button-press")
    .label(format!("Pressed {clicks} times"))
    .on_press(cx.listener(|this, _, _, cx| {
        this.button_clicks += 1;
        cx.notify();
    }))

Sx slot

Components with an `sx` slot also implement GPUI's `Styled` trait. Its methods and `sx` refine the same root after the variant and theme values; later calls win for properties both set. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("button-sx-sized")
            .label("200x56")
            // The slot owns the footprint: an enlargement, not
            // a shrink, so the label's own content size never
            // overflows the overridden box.
            .sx(|el| el.w(px(200.)).h(px(56.)))
            .into_any_element(),
        Button::new("button-sx-tinted")
            .label("Orange")
            .sx(|el| {
                el.bg(gpui::rgba(0xffa500ff))
                    .text_color(gpui::rgba(0x000000ff))
            })
            .into_any_element(),
        Button::new("button-styled-root")
            .label("Direct Styled")
            .bg(gpui::rgba(0x7828c8ff))
            .text_color(gpui::rgba(0xffffffff))
            .rounded(px(12.))
            .into_any_element(),
    ])

Custom hover fill

An `sx` background alone holds through hover, because the fade would otherwise ease the variant colour back over the override. `hover_bg` names the other end instead, so a caller-owned surface keeps the `transition-colors` fade. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_wrap()
    .items_center()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("button-hover-bg-sx")
            .label("Tinted hover")
            .sx(|el| {
                el.bg(gpui::rgba(0xffa500ff))
                    .text_color(gpui::rgba(0x000000ff))
            })
            .hover_bg(gpui::rgba(0xcc7000ff))
            .into_any_element(),
        Button::new("button-hover-bg-variant")
            .label("Variant resting fill")
            .variant(Variant::Secondary)
            // No `sx`: the fade still rests on the variant's
            // own colour and only the hover end is replaced.
            .hover_bg(gpui::rgba(0x7828c8ff))
            .into_any_element(),
    ])

Anatomy

Rust types

Button

Button is assembled from these builders. The API reference lists each one.

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Button looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Button styling
MethodValuesDescription
variant(Variant)Variant::Primary | Variant::Secondary | Variant::Tertiary | Variant::Outline | Variant::Ghost | Variant::Danger | Variant::DangerSoftVisual style variant, including a soft danger treatment.
size(Size)Size::Sm | Size::Md | Size::LgSize of the button.
full_width(bool)true | falseWhether the button fills its container.
is_icon_only(bool)true | falseUses the size-specific square icon control geometry.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 18 entries ported, 2 partial, 1 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Button builders
BuilderValuesDefaultDescription
variant(Variant)Variant::Primary | Variant::Secondary | Variant::Tertiary | Variant::Outline | Variant::Ghost | Variant::Danger | Variant::DangerSoftVariant::PrimaryVisual style variant, including a soft danger treatment.
size(Size)Size::Sm | Size::Md | Size::LgSize::MdSize of the button.
full_width(bool)true | falsefalseWhether the button fills its container.
is_disabled(bool)true | falsefalseDisables pointer and keyboard presses and removes the tab stop.
is_pending(bool)true | falsefalseBlocks presses and hover while retaining focusability.
is_icon_only(bool)true | falsefalseUses the size-specific square icon control geometry.
on_press(callback)PartialFn(e: ClickEvent) -> ()โ€”Pointer and keyboard presses are supported, but the callback receives GPUI's ClickEvent.
label(value) / child(element) / content(render)PartialAnyElement | Fn(values: ButtonRenderProps) -> AnyElementโ€”Text, arbitrary children and state-driven content are supported through separate builders.
content(render)true | falseโ€”Pending state supplied to custom content.
Not ported1 HeroUI entry not ported
Button builders not ported
HeroUI v3Why it is not ported
Button.renderOverrides the root element.

Parts

Button parts
PartDescription
ButtonFocusable root that owns content, visual variant and interaction state.

States

Button states
StateBuilderDescription
Hoveredis_hoveredVariant-specific hover background and render state.
Pressedis_pressedSize-specific scale, variant pressed-background endpoint and render state for pointer or keyboard press.
Focus visibletab_stop_handleKeyboard focus displays the status-focused ring.
Disabledis_disabledDisabled buttons are dimmed, inert and removed from the tab order.
Pendingis_pendingPending buttons are dimmed and inert but remain focusable.