Button
A pressable button with variants and states.
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.
`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.
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()])An element expression with window: &mut Window and cx: &mut App in scope.
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)
})
}))An element expression with window: &mut Window and cx: &mut App in scope.
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)
})
}))An element expression with window: &mut Window and cx: &mut App in scope.
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(),
])An element expression with window: &mut Window and cx: &mut App in scope.
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(),
])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.
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()])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>.
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()])An element expression with window: &mut Window and cx: &mut App in scope.
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"),
)`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.
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_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.
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(),
])`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.
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()])An element expression with window: &mut Window and cx: &mut App in scope.
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)
})
}))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.
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"),
)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.
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(),
]
})Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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();
}))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.
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(),
])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.
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 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
| Method | Values | Description |
|---|---|---|
variant(Variant) | Variant::Primary | Variant::Secondary | Variant::Tertiary | Variant::Outline | Variant::Ghost | Variant::Danger | Variant::DangerSoft | Visual style variant, including a soft danger treatment. |
size(Size) | Size::Sm | Size::Md | Size::Lg | Size of the button. |
full_width(bool) | true | false | Whether the button fills its container. |
is_icon_only(bool) | true | false | Uses the size-specific square icon control geometry. |
// 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
| Builder | Values | Default | Description |
|---|---|---|---|
variant( | Variant:: | Variant:: | Visual style variant, including a soft danger treatment. |
size( | Size:: | Size:: | Size of the button. |
full_width( | true | | false | Whether the button fills its container. |
is_disabled( | true | | false | Disables pointer and keyboard presses and removes the tab stop. |
is_pending( | true | | false | Blocks presses and hover while retaining focusability. |
is_icon_only( | true | | false | Uses the size-specific square icon control geometry. |
on_press(Partial | Fn( | โ | Pointer and keyboard presses are supported, but the callback receives GPUI's ClickEvent. |
label(Partial | AnyElement | | โ | Text, arbitrary children and state-driven content are supported through separate builders. |
content( | true | | โ | Pending state supplied to custom content. |
Not ported1 HeroUI entry not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Button.render | Overrides the root element. |
Parts
| Part | Description |
|---|---|
Button | Focusable root that owns content, visual variant and interaction state. |
States
| State | Builder | Description |
|---|---|---|
Hovered | is_hovered | Variant-specific hover background and render state. |
Pressed | is_pressed | Size-specific scale, variant pressed-background endpoint and render state for pointer or keyboard press. |
Focus visible | tab_stop_handle | Keyboard focus displays the status-focused ring. |
Disabled | is_disabled | Disabled buttons are dimmed, inert and removed from the tab order. |
Pending | is_pending | Pending buttons are dimmed and inert but remain focusable. |