# Several screens and dialogs

> Open sub-screens, go back, close the whole menu and ask yes-or-no questions with ConfirmDialog before destructive actions.

URL: https://xichil.dev/docs/xis-mvui/v1/guides/navigation/ · Updated: 2026-11-06 · Applies to: Fabric, Quilt, Legacy Fabric, Forge, NeoForge, MC 1.7.10 - 26.3

A menu can have any number of screens. This page shows how to move between them and how to confirm a
destructive action.

## The screen stack

The [UiHost](https://xichil.dev/javadoc/xis-mvui/v1/dev/xichil/mvui/UiHost.html) your `create` method receives keeps a stack of screens:

| Call | Effect |
| --- | --- |
| `host.open(screen)` | Pushes `screen` on top. Its `back()` returns to the current one. |
| `host.back()` | Pops the current screen. From the first screen, returns to wherever the menu was opened from (the Options screen, the mod list, or your own `parent`). |
| `host.close()` | Pops every screen at once, returning to wherever the menu was opened from. |

Keep the `host` (pass it to the methods that build screens) so buttons can navigate.

## Open a sub-screen

Build each screen in a method, and build it fresh every time it opens:

```java title="MyMenu.java"
@Override
public Screen create(UiHost host) {
    return MenuScreen.builder("main")
            .title(Text.translatable("mymod.title", "My Mod"))
            .add(Button.builder("advanced")
                    .label(Text.translatable("mymod.advanced", "Advanced..."))
                    .onPress(() -> host.open(advancedScreen(host)))
                    .build())
            .footer(Button.builder("done").label(UiTexts.DONE).onPress(host::back).build())
            .build();
}

private Screen advancedScreen(UiHost host) {
    return MenuScreen.builder("advanced")
            .title(Text.translatable("mymod.advanced.title", "Advanced"))
            .add(Toggle.builder("debug")
                    .label(Text.translatable("mymod.debug", "Debug output"))
                    .value(() -> config.debug)
                    .onChange(v -> config.debug = v)
                    .build())
            .footer(Button.builder("back").label(UiTexts.DONE).onPress(host::back).build())
            .build();
}
```

> **Warning:** Don't cache built screens across openings. A `TextInput`'s typed text and a `Section`'s expanded state live in
> the built objects, so a cached screen shows the last player's half-typed text. Screens are cheap to build.

Pass data into the screen-building method when a sub-screen is about one item:

```java
.onPress(() -> host.open(profileScreen(host, profiles.selected())))
```

## Ask before a destructive action

[ConfirmDialog](https://xichil.dev/javadoc/xis-mvui/v1/dev/xichil/mvui/ConfirmDialog.html) is a yes/no question on its own screen, drawn like
vanilla's "Delete world?" screen:

```java
StatusLine status = StatusLine.builder("status").build();

Button reset = Button.builder("reset")
        .label(Text.translatable("mymod.reset", "Reset..."))
        .onPress(() -> host.open(ConfirmDialog.builder("reset.confirm")
                .title(Text.translatable("mymod.reset.title", "Reset every setting?"))
                .message(Text.translatable("mymod.reset.message", "This can't be undone."))
                .yesLabel(Text.translatable("mymod.reset.yes", "Reset"))
                .noLabel(UiTexts.CANCEL)
                .onYes(() -> {
                    config.reset();
                    status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.reset.done", "Settings reset"));
                })
                .build()))
        .build();
```

| Builder method | Required | Default |
| --- | --- | --- |
| `title(Text)` | yes | |
| `message(Text)` | yes | |
| `onYes(Runnable)` | yes | |
| `yesLabel(Text)` | no | `UiTexts.YES`. A verb ("Delete", "Overwrite") is clearer. |
| `noLabel(Text)` | no | `UiTexts.NO` |
| `onNo(Runnable)` | no | does nothing; the dialog just closes |

When the player answers, the dialog **closes first** and then the answer's action runs. So the action runs with
the screen that opened the dialog as the current one: it can write to that screen's `StatusLine` (as above) or
call `host.open` to go somewhere else. That screen is refreshed afterwards.

## Leave the whole menu

`host.close()` from a deep screen returns straight to where the menu was opened, for example after a "Save
and exit" button:

```java
Button.builder("saveExit")
        .label(Text.translatable("mymod.saveExit", "Save and exit"))
        .onPress(() -> {
            config.save();
            host.close();
        })
        .build();
```

## Test it

`FakeHost` keeps the same stack. `host.current().id()` names the open screen and `host.depth()` counts the
stack. On a `ConfirmDialog`, click `yes` or `no`:

```java
host.click("reset");
assertEquals("reset.confirm", host.current().id());
host.click("yes");
assertEquals("main", host.current().id());
```

## Next steps

- [Slow work: files and network](https://xichil.dev/docs/xis-mvui/v1/guides/async-work/index.md): when a button's action takes time.
- [A list manager](https://xichil.dev/docs/xis-mvui/v1/examples/list-manager/index.md): a complete menu with sub-screens and a delete dialog.
