An Intro to Textual – Creating Text User Interfaces with Python

Textual is a Python package used to create cross-platform Text User Interfaces (TUI). This may sound like you’ll be creating a user interface with ASCII-art, but that is not the case.

Textual is quite advanced and allows you to add widgets to your terminal applications, including buttons, context switchers, scroll bars, checkboxes, inputs and more.

Getting Started

The first thing you need to do is install Textual. If you only want to run Textual applications, then the following pip comand is all you need:

python3 -m pip install textual

However, if you want to write your own Textual applications, you should run this command instead:

python3 -m pip install "textual[dev]"

That funny little [dev] part will install some extra dependencies that make developing Textual applications easier.

Run the Textual Demo

The Textual package comes with a demo. You’ll find the demo is a great way to see what types of things you can do with Textual.

Here’s how you can run the demo:

python3 -m textual

When you run the command above in your terminal, you should see something like the following appears:

Textual Demo App

You can explore Textual in the demo and see the following features:

  • Widgets
  • Accelerators (i.e. CTRL+C)
  • CSS styling
  • and more

Creating Your Own Simple Application

While the demo is a great place to start to get a feel for what you can do with Textual, it’s always better to dive into the docs and start writing some real code.

You will understand how things work much quicker if you write the code yourself and then start changing the code piece by piece. By going through this iterative process, you’ll learn how to build a little at a time and you’ll have a series of small failures and successes, which is a great way to build up your confidence as you learn.

The first step to take when creating a Textual application is to import Textual and subclass the App() class. When you subclass App(), you create a Textual application. The App() class contains all the little bits and bobs you need to create your very own terminal application.

To start off, create a new Python file in your favorite text editor or IDE and name it hello_textual.py.

Next, enter the following code into your new file:

from textual.app import App


class HelloWorld(App):
    ...


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

When you go to run your terminal application, you should run it in a terminal. Some IDEs have a terminal built-in, such as VS Code and PyCharm. Textual may or may not look correct in those terminals though.

Whenever possible, it is recommended that you run Textual applications in your external terminal. Your applications will look and behave better there most of the time. On Mac, it is recommended that you use iTerm rather than the built-in terminal as the built-in Terminal application hasn’t been updated in quite some time.

To run your new terminal application, you will need to run the following command:

python3 hello_textual.py

When you run this command, you will see the following:

Hello Textual

Oops! That’s kind of like creating a blank black box! That’s probably not what you want after all.

To exit a Textual application, press CTRL+C. When you do, you will exit the application and return to the normal terminal.

That was exciting, but the user interface was very plain. You can fix that up a bit by adding a label in the next section!

Adding a Label

Now that you are back to your original terminal, go back to your Python editor and create a new file. This time you will name it hello_textual2.py.

Enter the following code into your new Python file:

from textual.app import App, ComposeResult
from textual.widgets import Label


class HelloWorld(App):

    def compose(self) -> ComposeResult:
        yield Label("Hello Textual")

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

Your HelloWorld() class was empty before. Now you added a compose() method. The compose() method is where you normally setup your widgets.

A widget is a user interface element, such as a label, a text box, or a button. In this example, you add a Label() with the text “Hello World” in it.

Try running your new code in your terminal and you should see something like this:

Hello Textual with label

Well, that looks a little better than the original. But it would be nice to have a way to close your application without using CTRL+C.

One common way to close an application is with a Close button. You’ll learn how to add one of those next!

Adding a Close Button

When you create a user interface, you want to communicate with the user about how they can close your application. A terminal application already has a way to close the terminal itself by way of its exit button.

However, you usually want a way to close your Textual application without closing the terminal itself. You have been using CTRL+C for this.

But there’s a better way! You can add a Button widget and connect an event handler to it to close the function.

To get started, open up a new Python file in your Python editor of choice. Name this one hello_textual3.py and then enter the following code:

# hello_textual3.py

from textual.app import App, ComposeResult
from textual.widgets import Button, Label


class HelloWorld(App):

    def compose(self) -> ComposeResult:
        self.close_button = Button("Close", id="close")
        yield Label("Hello Textual")
        yield self.close_button

    def on_mount(self) -> None:
        self.screen.styles.background = "darkblue"
        self.close_button.styles.background = "red"

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.exit(event.button.id)

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

The first change you’ll encounter is that you are now importing a Button in addition to your Label.

The next change is that you are assigning the Button to self.close_button in your compose() function before yielding the button. By assigning the button to a class variable or attribute, you can more easily access it later to change some features around the widget.

The on_mount() method is called when your application enters application mode. Here you set the background of your app (the screen) to “darkblue” and you set the background color of the close button to “red”.

Lastly, you create an on_button_pressed() method, which is your event handler for catching when a button is pressed. When the close button is pressed, the on_button_pressed()  is called and your application exits. You pass in the button’s id to tell the application which button was used to close it, although you don’t use that information here.

It’s time to try running your code! When you do, you should see the following:

Hello Textual with close button

So far so good. Your application is looking great!

Now you’re ready to learn the basics of styling your application with CSS.

Adding Style with CSS

Textual let you apply a style using Cascading Style Sheet (CSS) in much the same way that web developers use CSS. You write the CSS file in a separate file that ends with the following extension: .css

By separating out the style from the logic, you can follow the Model-View-Controller design pattern. But even if you don’t follow that pattern, it lets you separate the logic from the design and can make iterating on your design easier.

To get started, you will first update your Python file so that it uses a CSS file. Open up your Python editor and create a new file named hello_textual_css.py, then enter the following code into it:

# hello_textual_css.py

from textual.app import App, ComposeResult
from textual.widgets import Button, Label


class HelloWorld(App):
    CSS_PATH = "hello.css"

    def compose(self) -> ComposeResult:
        self.close_button = Button("Close", id="close")
        yield Label("Hello Textual", id="hello")
        yield self.close_button

    def on_mount(self) -> None:
        self.screen.styles.background = "darkblue"
        self.close_button.styles.background = "red"

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.exit(event.button.id)

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

The only change here is to add the class attribute, CSS_PATH, right after your HelloWorld() class definition. The CSS_PATH can be a relative or absolute path to your CSS file.

In the example code above, you use a relative path to a file named hello.css which should be saved in the same folder as your Python file.

You can now create hello.css in your Python or text editor. Then enter the following code into it:

Screen {
    layout: grid;
    grid-size: 2;
    grid-gutter: 2;
    padding: 2;
}
#hello {
    width: 100%;
    height: 100%;
    column-span: 2;
    content-align: center bottom;
    text-style: bold;
}

Button {
    width: 100%;
    column-span: 2;
}

The Screen mentioned here maps to the self.screen object in your code. You are telling Textual that you want to use a grid layout where the number two signifies that the grid will be two columns wide and include two rows.

The spacing between rows is controlled by the grid-gutter. Finally, you set padding to add spacing around the content of the widget itself.

The #hello tag matches to the hello id of a widget in your code. In this case, your Label has the id of “hello”. So everything in the curly braces that follows the #hello tag controls the style of your Label. You want the label to span across both columns, and the text-style to be bold. You also set the width and height to 100%.

Finally, you have some styling to add to Button widgets. There’s only one here, but this would apply to all buttons if you had additional ones. You are setting the width of the button to 100% and telling it to span both columns.

Now that the explanation is out of the way, you are ready to try running your code. When you do, you should get something like this:

Hello Textual with CSS applied

The button is now nice and large, but you could certainly make the text of the label a bit bigger. You should try and figure out how to do that as a stretch goal!

Wrapping Up

Textual is amazing! The demo has many more examples than what is covered here as does the Textual documentation

Here is what you learned from this article:

  • The Textual Demo
  • Creating a Label
  • Adding a button
  • Using a layout
  • Styling with CSS

This article barely scratches the surface of all the amazing features that Textual has to offer. Keep an eye on this website though, as there are lots more articles on Textual coming soon!