Overlays

Modal

Display a dialog over the page content.

Rust
1use herogpui::components::modal::{Modal, ModalCloseTrigger, ModalSize};

Usage

Rust
1use herogpui::prelude::{Modal, ModalCloseTrigger, ModalSize};
2use gpui::prelude::*;
3
4stretch_col([
5    ("md-size-xs", "Xs", ModalSize::Xs),
6    ("md-size-sm", "Sm", ModalSize::Sm),
7    ("md-size-md", "Md", ModalSize::Md),
8    ("md-size-lg", "Lg", ModalSize::Lg),
9    ("md-size-cover", "Cover", ModalSize::Cover),
10    ("md-size-full", "Full", ModalSize::Full),
11]
12.into_iter()
13.map(|(key, label, size)| {
14    let open = self.demo_overlay(key);
15    overlay_demo(
16        open,
17        key,
18        label,
19        Modal::new()
20            .id(key)
21            .is_open(open)
22            .size(size)
23            .title(format!("Size: {label}"))
24            .is_dismissible(true)
25            .child(ModalCloseTrigger::new())
26            .child(gpui::div().child("Every size shares one panel style."))
27            .on_open_change(bool_cb(cx.listener(
28                move |this, v: &bool, _, cx| {
29                    this.set_demo_flag(key, *v);
30                    cx.notify();
31                },
32            )))
33            .into_any_element(),
34        cx,
35    )
36})
37.collect())

Live HeroGPUI compiled to WebAssembly. Select any example without loading another WASM instance.

Anatomy

Rust types

ModalModalCloseTrigger

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

Customization

Appearance builders and theme tokens Modal uses.

Styling

Modal styling
StyleDescription
scrim absolute inset_0 + bg + outside press on panelThe scrim fills the window but never grabs presses, because the panel's on_mouse_down_out owns backdrop dismissal; centres the container through the backdrop's flex, and centres through its own container.
Backdrop::Opaque -> colors.backdropDefault scrim colour.
Backdrop::Blur -> colors.backdrop alpha 0.6Gpui has no backdrop filter, so Blur renders a lighter scrim instead.
Backdrop::Transparent -> transparent_blackNo scrim at all.
util::window_overlay + p(px(40.)) + placement/scroll flex matchCovers the window even inside clipped or positioned containers, paints above later page content, and blocks pointer input to the page beneath. The desktop container keeps 40px padding and applies placement alignment inside.
overlay p(px(40.)).when(ModalSize::Full, p(px(0.)))Full removes the desktop container inset so its dialog reaches every viewport edge.
panel relative flex w_full + p(px(24.)) + overlay bg + overlay_shadow + container_radiusPanel surface: overlay background and shadow with the floating-panel radius and a uniform 24px inset.
Auto/Center -> container items_center justify_centerAuto reads centered from the sm step up, which is what draws for both values; the mobile mt-auto sheet branch is unreachable on a desktop app.
Top -> items_start / Bottom -> items_end container alignmentTop pins the panel to the container's start and bottom to its end.
panel max_h(panel_max) + overflow_hiddenCaps the dialog at the container's content box, retaining 40px of scrim even with overflowing content; Full removes that margin.
no panel cap + flex_shrink_0; container overflow_y_scroll under ModalScroll::OutsideThe dialog stays content-sized and cannot flex-shrink, so tall content creates scroll range on the surrounding container.
ModalSize::max_width match px(320./384./448./512.)Tailwind's width scale — 20rem, 24rem, 28rem, 32rem — transcribed as fixed pixels.
ModalSize::Cover -> w_full; Inside adds h_full + min_h_fullFills the definite-height Inside container on both axes while retaining the normal floating-panel radius and shadow; under Outside, v3's auto-height container leaves it content-sized.
ModalSize::Full -> w_full + no radius/shadow; Inside adds h_full + min_h_fullFills the edge-to-edge Inside container on both axes and drops the floating-panel radius and shadow; Outside remains content-sized so its container can scroll.
header flex flex_col + gap(px(12.))Title column with a 12px gap; draws it only when a title or icon is present and has no margin to reset.
text_size(px(16.)) + FontWeight::MEDIUM16px medium foreground title; align-middle has no gpui text equivalent.
icon size(px(40.)) + control_radius + role soft/soft_foreground40px glyph box with the control radius; tints it from a role colour, defaulting to v3's bg-default text-foreground, rather than composed children.
body flex flex_col + gap(px(10.)) + 14/20px muted14px/20px muted content; the -m/p pair is a focus-ring allowance and the inset is zero. Adds a 10px gap leaves to authors, and min-h-0/flex-1 have no effect in the port's layout.
body max_h(inside_body_max) + overflow_y_scrollThe body scrolls inside the panel's cap; gpui has no overscroll-behavior containment.
body unscrolled under ModalScroll::OutsideWith Outside, the body never scrolls and the container does.
footer flex items_center justify_end + gap(px(8.))End-aligned action row with an 8px gap; the margin is supplied by the sibling rule.
CloseButton absolute top(px(16.)) right(px(16.))Close button pinned 16px from the top end, outside the header.
body mt(px(8.)) + footer mt(px(20.))Sibling spacing: 8px below the header and 20px above the footer.
anim::Motion::BACKDROP_IN / BACKDROP_OUTThe scrim fades alone at 150ms in and 100ms out on ease-out.
anim::Motion::PANEL_IN entering_zoomThe panel settles down onto the page from 105%; it shrinks to fit, it does not grow from 90%.
anim::Motion::PANEL_OUT exitingThe exit is 100ms at the same curve.
container flex_col items_center justify_start + overflow_y_scrollMoves Outside scrolling to the backdrop and keeps the container pointer-transparent; scrolls the container itself and pins it to the window start — the same result, without the hit testing split.
entering_zoom / exiting with ZoomBox width and radius both NoneFull has no slide or geometric zoom in either implementation; only the shared fade remains.

API reference

Builders

Modal builders
BuilderTypeDefaultDescription
ParentElement::extend body onlyAnyElementTrigger and container 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_changeUseOverlayStateReturnControlled overlay state; takes the two builders the hook would feed rather than the hook return.
backdrop(Backdrop)BackdropBackdrop::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)booltrueClose on backdrop click; gates outside-press 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)boolfalseDisable ESC key to close.
is_open(bool)boolControlled open state.
on_open_change(callback)Fn(isOpen: bool) -> ()Open state change handler; fires with false on every dismissal path.
placement(ModalPlacement)ModalPlacementModalPlacement::AutoModal position on screen; Auto reads centered from v3's sm step up, which is a desktop app's only reading, while the mobile bottom-sheet branch is unreachable.
scroll(ModalScroll)ModalScrollModalScroll::InsideScroll behavior; both modes scroll, but v3's Inside caps the dialog at the container's content box while caps at the viewport, which only differs when the content overflows.
size(ModalSize)ModalSizeModalSize::MdModal size variant; the four max-width steps match, Cover and Full fill the width and Full drops the shadow, but also stretches both to the viewport height and keeps Full square.
title + footer_child + extend bodyAnyElement | Fn({close}) -> AnyElementContent or render function; feeds the body through ParentElement::extend, the title through title and the footer through footer_child, and the close argument is not a render value — the close affordance is the composed ModalCloseTrigger part.
title(value)AnyElementHeader content; renders its header only when a title or icon is set, and both share the monolithic prop.
ParentElement::extendAnyElementBody content; ParentElement::extend feeds the body, but v3's per-part prop surface is not exposed.
footer_child(el)elFooter content; repeated footer_child calls stack actions in the end-aligned row.
new() + ParentElement childrenAnyElementThe modal composes this part as a child: without children it draws the built-in CloseButton wired to the modal'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.

Parts

Modal parts
PartDescription
ModalOverlay root that owns open state and the focus trap; configured callbacks receive every enabled dismissal path.
ModalScrim behind the panel, dimmed but press-less; outside presses dismiss through the panel's own bounds.
ModalWindow-pinning wrapper that applies the placement alignment, the scroll mode and the 40px inset.
ModalThe panel itself: overlay surface, 24px inset, viewport cap for Inside scroll.
ModalTitle column with the icon; the monolithic port renders it only when a title or icon is set, without a composable children slot.
ModalGlyph box above the heading; takes an asset path and a role colour rather than composed children.
ModalTitle text; accepts a string via the title builder instead of composed children.
ModalContent area fed by ParentElement::extend children, scrollable in Inside mode.
ModalEnd-aligned action row fed by repeated footer_child calls.
ModalCloseTriggerComposed part pinned 16px from the top end: the default content is the built-in CloseButton wired to the modal'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.

States

Modal states
BuilderStateDescription
CloseButton tab_stop_handle ring + focus_handle + trap_tabFocusApplied to the trigger, the dialog and the close button; the close button rings from its tab stop and the dialog claims the panel's focus handle, while the trigger is not a modal part here.
CloseButton hover surfaceHoverApplied to the close button on hover.
CloseButton .active centered root-bounds shrinkActiveApplied 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.
OverlayPhase::Open branch + Motion::PANEL_INEnteringApplied during the modal opening animation; the panel zooms in from 105% at 250ms.
overlay_phase::Exiting + Motion::PANEL_OUTExitingApplied during the modal closing animation; the panel and scrim stay mounted for the 100ms exit.
ModalPlacement anchored matchPlacementAuto, top, center and bottom anchor the dialog and choose the container alignment.