Drawer
A slide-over panel anchored to a window edge.
use herogpui::components::drawer::{Drawer, DrawerCloseTrigger, DrawerPlacement};Drawer on docs.rsSource at v0.14.0
Usage
Showing Placement, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
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, Drawer, DrawerCloseTrigger, DrawerPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w_full()
.gap(px(12.))
.children([
("dr-left", "Left", DrawerPlacement::Left),
("dr-right", "Right", DrawerPlacement::Right),
("dr-top", "Top", DrawerPlacement::Top),
("dr-bottom", "Bottom", DrawerPlacement::Bottom),
]
.into_iter()
.map(|(key, label, placement)| {
let open = self.demo_overlay(key);
overlay_demo(
open,
key,
label,
Drawer::new()
.id(key)
.is_open(open)
.placement(placement)
.title(format!("From the {label}"))
.is_dismissible(true)
.child(DrawerCloseTrigger::new())
.child(gpui::div().child("The panel slides in along its edge."))
.on_open_change(cx.listener(move |this, v: &bool, _, cx| {
this.set_demo_flag(key, *v);
cx.notify();
}))
.into_any_element(),
cx,
)
}))Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
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, Drawer, DrawerCloseTrigger, DrawerPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;
overlay_demo(
self.demo_overlay("dr-no-dismiss"),
"dr-no-dismiss",
"Open a non-dismissable drawer",
Drawer::new()
.id("drawer-no-dismiss")
.is_open(self.demo_overlay("dr-no-dismiss"))
.title("Finish first")
.is_dismissible(false)
.is_keyboard_dismiss_disabled(true)
.child(gpui::div().child("The backdrop and Escape are both inert."))
.footer_child(
Button::new("drawer-no-dismiss-ok").label("Done").on_press(
cx.listener(|this, _, _, cx| {
this.set_demo_flag("dr-no-dismiss", false);
cx.notify();
}),
),
)
.on_open_change(cx.listener(|this, v: &bool, _, cx| {
this.set_demo_flag("dr-no-dismiss", *v);
cx.notify();
}))
.into_any_element(),
cx,
)Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
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, Drawer, DrawerCloseTrigger, DrawerPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;
overlay_demo(
self.demo_overlay("dr-scroll"),
"dr-scroll",
"Open a long drawer",
Drawer::new()
.id("drawer-scroll")
.is_open(self.demo_overlay("dr-scroll"))
.title("Release notes")
.is_dismissible(true)
.child(DrawerCloseTrigger::new())
.child(gpui::div().flex().flex_col().gap(px(8.)).children(
(1..=20).map(|n| {
gpui::div().child(format!("Change {n} of twenty."))
}),
),)
.on_open_change(cx.listener(|this, v: &bool, _, cx| {
this.set_demo_flag("dr-scroll", *v);
cx.notify();
}))
.into_any_element(),
cx,
)Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
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, Drawer, DrawerCloseTrigger, DrawerPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, App, Context, px};
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w_full()
.gap(px(12.))
.children(vec![
para(
&format!(
"The flag lives with the caller: {}",
if self.demo_overlay("dr-controlled") {
"open"
} else {
"closed"
}
),
cx,
),
overlay_demo(
self.demo_overlay("dr-controlled"),
"dr-controlled",
"Open (controlled)",
Drawer::new()
.id("drawer-controlled")
.is_open(self.demo_overlay("dr-controlled"))
.title("Controlled")
.is_dismissible(true)
.child(DrawerCloseTrigger::new())
.child(gpui::div().child("Closing reports through onOpenChange."))
.on_open_change(cx.listener(|this, v: &bool, _, cx| {
this.set_demo_flag("dr-controlled", *v);
cx.notify();
}))
.into_any_element(),
cx,
),
])Also calls para, overlay_min_h, el_id, overlay_demo (included when you copy).
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, Drawer, DrawerCloseTrigger, DrawerPlacement, TextArea, TextField, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;
overlay_demo(
self.demo_overlay("dr-form"),
"dr-form",
"Open a form drawer",
Drawer::new()
.id("drawer-form")
.is_open(self.demo_overlay("dr-form"))
.title("New issue")
.is_dismissible(true)
.child(DrawerCloseTrigger::new())
.child(
gpui::div()
.flex()
.flex_col()
.gap(px(12.))
.child(
TextField::new(self.demo_text("dr-form-title", "", cx))
.label("Title"),
)
.child(
TextArea::new(self.demo_text("dr-form-body", "", cx))
.label("Description")
.rows(3),
),
)
.footer_child(Button::new("drawer-form-save").label("Create").on_press(
cx.listener(|this, _, _, cx| {
this.set_demo_flag("dr-form", false);
cx.notify();
}),
))
.on_open_change(cx.listener(|this, v: &bool, _, cx| {
this.set_demo_flag("dr-form", *v);
cx.notify();
}))
.into_any_element(),
cx,
)Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Backdrop, Button, Drawer, DrawerCloseTrigger, DrawerPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.w_full()
.gap(px(12.))
.children(herogpui::core::Backdrop::ALL
.iter()
.map(|backdrop| {
let key: &'static str = match backdrop {
herogpui::core::Backdrop::Opaque => "dr-bd-opaque",
herogpui::core::Backdrop::Blur => "dr-bd-blur",
herogpui::core::Backdrop::Transparent => "dr-bd-transparent",
};
let open = self.demo_overlay(key);
overlay_demo(
open,
key,
backdrop.label(),
Drawer::new()
.id(key)
.is_open(open)
.backdrop(*backdrop)
.title(format!("Backdrop: {}", backdrop.label()))
.is_dismissible(true)
.child(DrawerCloseTrigger::new())
.child(gpui::div().child("The scrim behind the panel."))
.on_open_change(cx.listener(
move |this, v: &bool, _, cx| {
this.set_demo_flag(key, *v);
cx.notify();
},
))
.into_any_element(),
cx,
)
}))Also calls overlay_min_h, el_id, overlay_demo (included when you copy).
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, Drawer, DrawerCloseTrigger, DrawerPlacement};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let is_open = self.drawer_open;
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_center()
.w_full(),
is_open,
240.,
)
.child(
Button::new("drawer-open")
.label("Open drawer")
.on_press(cx.listener(|this, _, _, cx| {
this.drawer_open = true;
cx.notify();
}),)
)
.child(
Drawer::new()
.id("drawer")
.is_open(is_open)
.title("Settings")
.placement(DrawerPlacement::Right)
.child(DrawerCloseTrigger::new())
.child(gpui::div().child("Panel content goes here."))
.footer_child(Button::new("drawer-done").label("Done").on_press(
cx.listener(|this, _, _, cx| {
this.drawer_open = false;
cx.notify();
}),
))
.on_close(cx.listener(|this, _, _, cx| {
this.drawer_open = false;
cx.notify();
})),
)Also calls overlay_min_h (included when you copy).
Helpers used by these examples
Plain functions over the public API, defined once here and copied with each example that calls them.
fn overlay_min_h(mut frame: gpui::Div, open: bool, height: f32) -> gpui::Div {
if open {
frame = frame.min_h(px(height));
}
frame
}fn el_id(s: String) -> gpui::ElementId {
gpui::ElementId::Name(s.into())
}/// One overlay demo: the trigger, and the panel it opens.
///
/// An overlay needs a positioned ancestor and enough height to show the panel,
/// and each demo owns its own open flag -- v3's pages show one variant per
/// example, so a shared flag would open all of them at once.
fn overlay_demo(
open: bool,
key: &'static str,
label: &str,
panel: AnyElement,
cx: &mut Context<'_, Gallery>,
) -> AnyElement {
// The panel is `absolute inset-0` inside this frame, and v3's body is
// `min-h-0 flex-1`, so a short frame squeezes the body to nothing.
// Reserve that height only while the panel is open; a closed trigger
// sitting in a 320px hole was the empty-card gap on overlay pages.
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_center()
.w_full(),
open,
320.,
)
.child(
Button::new(format!("{key}-open"))
.label(label.to_owned())
.variant(Variant::Secondary)
.on_press(cx.listener(move |this, _, _, cx| {
this.set_demo_flag(key, true);
cx.notify();
})),
)
.child(panel)
.into_any_element()
}fn para(text: &str, cx: &App) -> gpui::AnyElement {
let colors = cx.colors();
gpui::div()
.text_size(px(14.5))
.line_height(px(24.))
.text_color(colors.foreground)
.max_w(px(720.))
.child(text.to_owned())
.into_any_element()
}Anatomy
Rust types
Drawer 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 Drawer 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 |
|---|---|---|
backdrop(Backdrop)Partial | Backdrop::Opaque | Backdrop::Blur | Backdrop::Transparent | Backdrop overlay style; all three variants render, but gpui has no backdrop-filter, so Blur draws a lighter scrim rather than a blur. |
placement(DrawerPlacement) | DrawerPlacement::Top | DrawerPlacement::Bottom | DrawerPlacement::Left | DrawerPlacement::Right | Edge the drawer slides from. |
// 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, 12 partial, 15 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 |
|---|---|---|---|
ParentElement::Partial | AnyElement | — | Trigger and backdrop elements; the port's ParentElement::extend feeds the body instead, with the backdrop built in and the trigger left to the caller. |
is_open + on_open_changePartial | UseOverlayStateReturn | — | Controlled overlay state; takes the two builders the hook would feed rather than the hook return. |
backdrop(Partial | Backdrop:: | Backdrop:: | Backdrop overlay style; all three variants render, but gpui has no backdrop-filter, so Blur draws a lighter scrim rather than a blur. |
is_dismissible(Partial | true | | true | Close on backdrop click; gates outside-press and drag dismissal on it. The composed close trigger is not the backdrop: like it renders and closes regardless of this flag. |
is_keyboard_dismiss_disabled( | true | | false | Disable ESC key to close. |
is_open( | true | | — | Controlled open state. |
on_open_change( | Fn( | — | Open state change handler; fires with false on every dismissal path. |
placement( | DrawerPlacement:: | DrawerPlacement:: | Edge the drawer slides from. |
title + footer_child + extend bodyPartial | AnyElement | — | Dialog content; header, body and footer compose through title, ParentElement::extend and footer_child instead of nested children. |
title(Partial | AnyElement | — | Header content; renders its header only when a title is set, and both share the monolithic prop. |
ParentElement::Partial | AnyElement | — | Body content; ParentElement::extend feeds the scrollable body, but v3's per-part prop surface is not exposed. |
footer_child( | AnyElement | — | Footer content; repeated footer_child calls stack actions in the end-aligned row. |
new( | AnyElement | — | The drawer composes this part as a child: without children it draws the built-in CloseButton wired to the drawer's dismissal paths, and custom children replace the CloseButton's glyph while the press stays automatically wired to close — hands the composed part v3's slot="close" chaining. |
Not ported14 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Drawer.Trigger.children | Custom trigger content; renders no trigger slot, so callers drive is_open from their own control. |
Drawer.Trigger.className | Styles for the trigger. |
Drawer.Backdrop.className | Backdrop styles. |
Drawer.Content.className | Content styles. |
Drawer.Dialog.className | Styles for the dialog. |
Drawer.Dialog.role | ARIA role override; gpui exposes no accessibility tree. |
Drawer.Dialog.aria-label | Accessibility label; gpui exposes no accessibility tree. |
Drawer.Dialog.aria-labelledby | ID of the label element; gpui exposes no accessibility tree. |
Drawer.Header.className | Styles for the header. |
Drawer.Heading.className | Styles for the heading. |
Drawer.Body.className | Styles for the body. |
Drawer.Footer.className | Styles for the footer. |
Drawer.Handle.className | Styles for the drag handle bar. |
Drawer.CloseTrigger.className | Close trigger styles. |
Parts
| Part | Description |
|---|---|
Drawer | Overlay root that owns open state, the focus trap and every dismissal path. |
Drawer | Scrim behind the sheet, dimmed but press-less; outside presses dismiss through the panel's own bounds. |
Drawer | Full-window positioning wrapper with the four placement alignments. |
Drawer | The sheet itself: overlay surface, 24px inset, drag and scroll surfaces. |
DrawerPartial | Title column; the monolithic port renders it only when a title is set, without a composable children slot. |
DrawerPartial | Title text; accepts a string via the title builder instead of composed children. |
Drawer | Native-scrolling content area fed by ParentElement::extend children. |
Drawer | End-aligned action row fed by repeated footer_child calls. |
Drawer | Separator bar that starts the drag-to-dismiss gesture. |
DrawerCloseTrigger | Composed part pinned 16px from the top end: the default content is the built-in CloseButton wired to the drawer's dismissal paths regardless of is_dismissible, and custom children replace the glyph while staying wired to close. With no dismissal callback to wire the part draws nothing. |
Not ported1 HeroUI entry not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Drawer.Trigger | Element that opens the drawer; renders no trigger slot, so callers drive is_open from their own control. |
States
| State | Builder | Description |
|---|---|---|
FocusPartial | CloseButton tab_stop_handle ring | Applied to the trigger and close button; the built-in close button rings, while the trigger is not a drawer part here. |
Hover | CloseButton hover surface | Applied to the close button on hover. |
ActivePartial | CloseButton .active centered root-bounds shrink | Applied to the trigger and close button when pressed; the close button uses a centered root-bounds shrink while fixed child content remains unscaled, and the absent trigger has no surface. |
Entering | OverlayPhase::Open branch | Applied during the drawer opening animation. |
Exiting | overlay_phase::Exiting | Applied during the drawer closing animation; the sheet stays mounted for its 200ms slide. |
Placement | DrawerPlacement anchored match | Top, bottom, left and right anchor the sheet and choose the slide edge. |