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

Overlays

Drawer

A slide-over panel anchored to a window edge.

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

Placement

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, 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).

Non-Dismissable

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, 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).

Scrollable Content

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, 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).

Controlled State

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, 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).

With Form

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, 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).

Navigation Drawer

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, Drawer, DrawerCloseTrigger, DrawerPlacement, ListBox, ListBoxItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{AnyElement, Context, px};
use herogpui::gpui;

overlay_demo(
    self.demo_overlay("dr-nav"),
    "dr-nav",
    "Open the navigation",
    Drawer::new()
        .id("drawer-nav")
        .is_open(self.demo_overlay("dr-nav"))
        .placement(DrawerPlacement::Left)
        .title("Menu")
        .is_dismissible(true)
        .child(DrawerCloseTrigger::new())
        .child(ListBox::new(
            "drawer-nav-list",
            vec![
                ListBoxItem::new("home", "Home"),
                ListBoxItem::new("projects", "Projects"),
                ListBoxItem::new("settings", "Settings"),
                ListBoxItem::separator(),
                ListBoxItem::new("logout", "Log out").danger(),
            ],
        ))
        .on_open_change(cx.listener(|this, v: &bool, _, cx| {
            this.set_demo_flag("dr-nav", *v);
            cx.notify();
        }))
        .into_any_element(),
    cx,
)

Also calls overlay_min_h, el_id, overlay_demo (included when you copy).

Backdrop Variants

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::{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).

Usage

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

overlay_min_h
fn overlay_min_h(mut frame: gpui::Div, open: bool, height: f32) -> gpui::Div {
    if open {
        frame = frame.min_h(px(height));
    }
    frame
}
el_id
fn el_id(s: String) -> gpui::ElementId {
    gpui::ElementId::Name(s.into())
}
overlay_demo
/// 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()
}
para
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

DrawerDrawerCloseTrigger

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

Drawer styling
MethodValuesDescription
backdrop(Backdrop)PartialBackdrop::Opaque | Backdrop::Blur | Backdrop::TransparentBackdrop 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::RightEdge the drawer slides from.
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, 12 partial, 15 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Drawer builders
BuilderValuesDefaultDescription
ParentElement::extend body onlyPartialAnyElement—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_changePartialUseOverlayStateReturn—Controlled overlay state; takes the two builders the hook would feed rather than the hook return.
backdrop(Backdrop)PartialBackdrop::Opaque | Backdrop::Blur | Backdrop::TransparentBackdrop::OpaqueBackdrop overlay style; all three variants render, but gpui has no backdrop-filter, so Blur draws a lighter scrim rather than a blur.
is_dismissible(bool)Partialtrue | falsetrueClose 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(bool)true | falsefalseDisable ESC key to close.
is_open(bool)true | false—Controlled open state.
on_open_change(callback)Fn(isOpen: bool) -> ()—Open state change handler; fires with false on every dismissal path.
placement(DrawerPlacement)DrawerPlacement::Top | DrawerPlacement::Bottom | DrawerPlacement::Left | DrawerPlacement::RightDrawerPlacement::BottomEdge the drawer slides from.
title + footer_child + extend bodyPartialAnyElement—Dialog content; header, body and footer compose through title, ParentElement::extend and footer_child instead of nested children.
title(value)PartialAnyElement—Header content; renders its header only when a title is set, and both share the monolithic prop.
ParentElement::extendPartialAnyElement—Body content; ParentElement::extend feeds the scrollable body, but v3's per-part prop surface is not exposed.
footer_child(el)AnyElement—Footer content; repeated footer_child calls stack actions in the end-aligned row.
new() + ParentElement childrenAnyElement—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
Drawer builders not ported
HeroUI v3Why it is not ported
Drawer.Trigger.childrenCustom trigger content; renders no trigger slot, so callers drive is_open from their own control.
Drawer.Trigger.classNameStyles for the trigger.
Drawer.Backdrop.classNameBackdrop styles.
Drawer.Content.classNameContent styles.
Drawer.Dialog.classNameStyles for the dialog.
Drawer.Dialog.roleARIA role override; gpui exposes no accessibility tree.
Drawer.Dialog.aria-labelAccessibility label; gpui exposes no accessibility tree.
Drawer.Dialog.aria-labelledbyID of the label element; gpui exposes no accessibility tree.
Drawer.Header.classNameStyles for the header.
Drawer.Heading.classNameStyles for the heading.
Drawer.Body.classNameStyles for the body.
Drawer.Footer.classNameStyles for the footer.
Drawer.Handle.classNameStyles for the drag handle bar.
Drawer.CloseTrigger.classNameClose trigger styles.

Parts

Drawer parts
PartDescription
DrawerOverlay root that owns open state, the focus trap and every dismissal path.
DrawerScrim behind the sheet, dimmed but press-less; outside presses dismiss through the panel's own bounds.
DrawerFull-window positioning wrapper with the four placement alignments.
DrawerThe sheet itself: overlay surface, 24px inset, drag and scroll surfaces.
DrawerPartialTitle column; the monolithic port renders it only when a title is set, without a composable children slot.
DrawerPartialTitle text; accepts a string via the title builder instead of composed children.
DrawerNative-scrolling content area fed by ParentElement::extend children.
DrawerEnd-aligned action row fed by repeated footer_child calls.
DrawerSeparator bar that starts the drag-to-dismiss gesture.
DrawerCloseTriggerComposed 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
Drawer parts not ported
HeroUI v3Why it is not ported
Drawer.TriggerElement that opens the drawer; renders no trigger slot, so callers drive is_open from their own control.

States

Drawer states
StateBuilderDescription
FocusPartialCloseButton tab_stop_handle ringApplied to the trigger and close button; the built-in close button rings, while the trigger is not a drawer part here.
HoverCloseButton hover surfaceApplied to the close button on hover.
ActivePartialCloseButton .active centered root-bounds shrinkApplied 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.
EnteringOverlayPhase::Open branchApplied during the drawer opening animation.
Exitingoverlay_phase::ExitingApplied during the drawer closing animation; the sheet stays mounted for its 200ms slide.
PlacementDrawerPlacement anchored matchTop, bottom, left and right anchor the sheet and choose the slide edge.