Skip to content

Modal

Modal is the low-level modal component. It is responsible for only three things: drawing the mask, placing content, and providing the current input layer to the subtree while open. Confirm buttons, close keys, scrolling, and form submission are composed by you in the outer component or child components.

modal demo

This recording is generated by docs/tapes/modal.tape. The example first moves a background list, then opens a modal. While the modal is open, pressing j only updates modal state; the background list does not keep moving. It then closes with Esc, opens again, and accepts the current task with Enter.

cargo run --example modal

In the background state, press j/k to move and m to open the modal. Inside the modal, j/k are consumed by the modal, Enter accepts, Esc or c closes, and q quits.

If child components inside the modal register their own EventScope::Current handlers, let Modal create the input layer itself:

Modal(
    open: open.get(),
    width: Constraint::Length(68),
    height: Constraint::Length(12),
    style: Style::new().dim(),
) {
    Border(top_title: Line::from("Details").centered()) {
        Text(text: Line::from("Modal content"))
    }
}

Modal(open: false) does not render content and does not update its subtree. When opened, it creates a default blocks_lower=true layer and injects that layer into the subtree. SearchInput, Select, ScrollView, or custom use_event_handler(EventScope::Current, ...) calls inside the subtree naturally belong to the modal layer.

When modal key logic lives in the parent component, have the parent own the layer and pass that same layer to Modal:

let layer = hooks.use_input_layer(modal_open.get(), true);

hooks.use_event_handler(
    EventScope::Layer(layer),
    EventPriority::High,
    move |event| {
        // handle Enter / Esc / modal-only shortcuts
        EventResult::Consumed
    },
);

Modal(
    open: modal_open.get(),
    layer: Some(layer),
    width: Constraint::Length(68),
    height: Constraint::Length(12),
) {
    // modal content
}

Do not split this pair apart. use_event_handler(EventScope::Layer(layer)) binds to the layer created by the parent; Modal(layer: Some(layer)) tells the modal to reuse that layer and inject it into the subtree. If you forget to pass layer, Modal creates another higher layer and the parent handler will not receive modal keys.

blocks_lower defaults to the equivalent of Some(true), which fits real modal dialogs:

Modal(open: open.get(), blocks_lower: Some(true)) {
    // lower layers are blocked
}

For non-blocking floating layers, temporary hints, or debug panels, pass blocks_lower: Some(false). That only means lower layers may still receive events; if an upper child component returns EventResult::Consumed, propagation still stops.

Modal itself does not decide which keys close the dialog for you. The low-level modal fits complex interaction. For ordinary confirmation, alerts, and shortcut help, prefer the built-in ConfirmModal, AlertModal, and ShortcutInfoModal.

width and height decide the content region size, while placement decides where that content appears in the terminal:

Modal(
    open: open.get(),
    placement: Placement::BottomRight,
    margin: Margin::new(2, 2),
    offset: Offset { x: -1, y: -1 },
    width: Constraint::Length(48),
    height: Constraint::Length(10),
    style: Style::new().dim(),
) {
    // content
}

style applies to the full-screen mask. The modal content style is decided by the subtree. A common pattern is putting a Border inside Modal: the outer Modal controls mask and placement, and the inner Border controls title, border, padding, and concrete layout.

When a modal needs long content, place ScrollView inside the Modal subtree. Its Current handler automatically belongs to the modal layer.