Creating a Modal Dialog For Your TUIs in Textual

Textual is a Python package that you can use to create beautiful text-based user interfaces (TUIs). In other words, you can create a GUI in your terminal with Textual.

In this tutorial, you will learn how to create a modal dialog in your terminal. Dialogs are great ways to alert the user about something or get some input. You also see dialogs used for such things as settings or help.

Let’s get started!

Adding a Modal Dialog

The first step is to think up some kind of application that needs a dialog. For this tutorial, you will create a form allowing the user to enter a name and address. Your application won’t save any data as that is outside the scope of this article, but it will demonstrate how to create the user interface and you can add that functionality yourself later on, if you wish.

To start, create a new file and name it something like tui_form.py. Then enter the following code:

from textual.app import App, ComposeResult
from textual.containers import Center
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Input, Static, Label


class QuitScreen(ModalScreen):
    """Screen with a dialog to quit."""

    def compose(self) -> ComposeResult:
        yield Grid(
            Label("Are you sure you want to quit?", id="question"),
            Button("Quit", variant="error", id="quit"),
            Button("Cancel", variant="primary", id="cancel"),
            id="dialog",
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "quit":
            self.app.exit()
        else:
            self.app.pop_screen()


class Form(Static):
    def compose(self) -> ComposeResult:
        """
        Creates the main UI elements
        """
        yield Input(id="first_name", placeholder="First Name")
        yield Input(id="last_name", placeholder="Last Name")
        yield Input(id="address", placeholder="Address")
        yield Input(id="city", placeholder="City")
        yield Input(id="state", placeholder="State")
        yield Input(id="zip_code", placeholder="Zip Code")
        yield Input(id="email", placeholder="email")
        with Center():
            yield Button("Save", id="save_button")


class AddressBookApp(App):
    CSS_PATH = "modal.tcss"
    BINDINGS = [("q", "request_quit", "Quit")]

    def compose(self) -> ComposeResult:
        """
        Lays out the main UI elemens plus a header and footer
        """
        yield Header()
        yield Form()
        yield Footer()

    def action_request_quit(self) -> None:
        """Action to display the quit dialog."""
        self.push_screen(QuitScreen())


if __name__ == "__main__":
    app = AddressBookApp()
    app.run()

Since this code is a little long, you will review it piece by piece. You will start at the bottom since the main entry point is the AddressBookApp class.

Here’s the code:

class AddressBookApp(App):
    CSS_PATH = "modal.tcss"
    BINDINGS = [("q", "request_quit", "Quit")]

    def compose(self) -> ComposeResult:
        """
        Lays out the main UI elemens plus a header and footer
        """
        yield Header()
        yield Form()
        yield Footer()

    def action_request_quit(self) -> None:
        """Action to display the quit dialog."""
        self.push_screen(QuitScreen())


if __name__ == "__main__":
    app = AddressBookApp()
    app.run()

The AddressBookApp class is your application code. Here, you create a header, the form itself, and the footer of your application. When the user presses the q button, you also set up an accelerator key or key binding that calls action_request_quit(). This will cause your modal dialog to appear!

But before you look at that code, you should check out your Form’s code:

class Form(Static):
    def compose(self) -> ComposeResult:
        """
        Creates the main UI elements
        """
        yield Input(id="first_name", placeholder="First Name")
        yield Input(id="last_name", placeholder="Last Name")
        yield Input(id="address", placeholder="Address")
        yield Input(id="city", placeholder="City")
        yield Input(id="state", placeholder="State")
        yield Input(id="zip_code", placeholder="Zip Code")
        yield Input(id="email", placeholder="email")
        with Center():
            yield Button("Save", id="save_button")

The Form class has a series of Input() widgets, which are text boxes, that you can enter your address information into. You specify a unique id for each widgets to give you a way to style each of them. You can read more about how that works in Using CSS to Style a Python TUI with Textual. The placeholder parameter lets you add a label to the text control so the user knows what should be entered in the widget.

Now you are ready to move on and look at your modal dialog code:

class QuitScreen(ModalScreen):
    """Screen with a dialog to quit."""

    def compose(self) -> ComposeResult:
        yield Grid(
            Label("Are you sure you want to quit?", id="question"),
            Button("Quit", variant="error", id="quit"),
            Button("Cancel", variant="primary", id="cancel"),
            id="dialog",
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "quit":
            self.app.exit()
        else:
            self.app.pop_screen()

The QuitScreen class is made up of two functions:

  • compose() – Creates the widgets in the modal dialog
  • on_button_pressed() – The event handler that is fired when a button is pressed

The application will exit if the user presses the “Quit” button. Otherwise, the dialog will be dismissed, and you’ll return to your form screen.

You can style your application, including the modal dialog, using CSS. If you scroll back up to your application class, you’ll see that it refers to a CSS_PATH that points to a file named modal.tcss.

Here’s the CSS code that you’ll need to add to that file:

QuitScreen {
    align: center middle;
}

#dialog {
    grid-size: 2;
    grid-gutter: 1 2;
    grid-rows: 1fr 3;
    padding: 0 1;
    width: 60;
    height: 11;
    border: thick $background 80%;
    background: $surface;
}

#question {
    column-span: 2;
    height: 1fr;
    width: 1fr;
    content-align: center middle;
}

Button {
    width: 100%;
}

This CSS will set the size and location of your modal dialog on the application. You tell Textual that you want the buttons to be centered and the dialog to be centered in the application.

To run your code, open up your terminal (or cmd.exe / Powershell on Windows), navigate to the folder that has your code in it, and run this command:

python tui_form.py

When you run this command, the initial screen will look like this:

Textual Form with quit

To see the dialog, click the Save button to take the focus out of the text controls. Then hit the q button, and you will see the following:

Textual form with modal dialog

You’ve done it! You created a modal dialog in your terminal!

Wrapping Up

Textual is a great Python package. You can create rich, expressive user interfaces in your terminal. These GUIs are also lightweight and can be run in your browser via Textual-web.

Want to learn more about Textual? Check out some of the following tutorials: