How to design a fully interactive, reactive and dynamic terminal-based data dashboard using text?

In this tutorial, we build an advanced interactive dashboard using textwe explore how a terminal-first UI framework can be as expressive and dynamic as a modern web dashboard. As we write and run each code snippet, we actively build the interface, widgets, layouts, reactive states, and event flows piece by piece, so we can see how Textual behaves like a real-time UI engine in Google Colab. Finally, we noticed how naturally we could blend tables, trees, forms, and progress indicators into a cohesive application that felt fast, clean, and responsive. Check The complete code is here.

!pip install textual textual-web nest-asyncio


from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import (
   Header, Footer, Button, DataTable, Static, Input,
   Label, ProgressBar, Tree, Select
)
from textual.reactive import reactive
from textual import on
from datetime import datetime
import random


class StatsCard(Static):
   value = reactive(0)
  
   def __init__(self, title: str, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.title = title
      
   def compose(self) -> ComposeResult:
       yield Label(self.title)
       yield Label(str(self.value), id="stat-value")
  
   def watch_value(self, new_value: int) -> None:
       if self.is_mounted:
           try:
               self.query_one("#stat-value", Label).update(str(new_value))
           except Exception:
               pass

We set up the environment and import all the necessary components to build our text application. When we defined the StatsCard widget, we established a reusable component that reacts to value changes and automatically updates itself. We’re starting to see how Textual’s reactive system lets us create dynamic UI elements with minimal effort. Check The complete code is here.

class DataDashboard(App):
   CSS = """
   Screen { background: $surface; }
   #main-container { height: 100%; padding: 1; }
   #stats-row { height: auto; margin-bottom: 1; }
   StatsCard { border: solid $primary; height: 5; padding: 1; margin-right: 1; width: 1fr; }
   #stat-value { text-style: bold; color: $accent; content-align: center middle; }
   #control-panel { height: 12; border: solid $secondary; padding: 1; margin-bottom: 1; }
   #data-section { height: 1fr; }
   #left-panel { width: 30; border: solid $secondary; padding: 1; margin-right: 1; }
   DataTable { height: 100%; border: solid $primary; }
   Input { margin: 1 0; }
   Button { margin: 1 1 1 0; }
   ProgressBar { margin: 1 0; }
   """
  
   BINDINGS = [
       ("d", "toggle_dark", "Toggle Dark Mode"),
       ("q", "quit", "Quit"),
       ("a", "add_row", "Add Row"),
       ("c", "clear_table", "Clear Table"),
   ]
  
   total_rows = reactive(0)
   total_sales = reactive(0)
   avg_rating = reactive(0.0)

We define the DataDashboard class and configure global styles, keybindings, and reactive properties. We decide from the top how the application will look and behave, giving us full control over themes and interactivity. This structure helps us create a beautiful dashboard without writing any HTML or JS. Check The complete code is here.

  def compose(self) -> ComposeResult:
       yield Header(show_clock=True)
      
       with Container(id="main-container"):
           with Horizontal(id="stats-row"):
               yield StatsCard("Total Rows", id="card-rows")
               yield StatsCard("Total Sales", id="card-sales")
               yield StatsCard("Avg Rating", id="card-rating")
          
           with Vertical(id="control-panel"):
               yield Input(placeholder="Product Name", id="input-name")
               yield Select(
                   [("Electronics", "electronics"),
                    ("Books", "books"),
                    ("Clothing", "clothing")],
                   prompt="Select Category",
                   id="select-category"
               )
               with Horizontal():
                   yield Button("Add Row", variant="primary", id="btn-add")
                   yield Button("Clear Table", variant="warning", id="btn-clear")
                   yield Button("Generate Data", variant="success", id="btn-generate")
               yield ProgressBar(total=100, id="progress")
          
           with Horizontal(id="data-section"):
               with Container(id="left-panel"):
                   yield Label("Navigation")
                   tree = Tree("Dashboard")
                   tree.root.expand()
                   products = tree.root.add("Products", expand=True)
                   products.add_leaf("Electronics")
                   products.add_leaf("Books")
                   products.add_leaf("Clothing")
                   tree.root.add_leaf("Reports")
                   tree.root.add_leaf("Settings")
                   yield tree
              
               yield DataTable(id="data-table")
      
       yield Footer()

We build the entire UI layout, arranging containers, cards, form inputs, buttons, navigation trees and data tables. As we build these components, we’ll see the interface form exactly how we envisioned it. This snippet allows us to design the visual framework of the dashboard in a clean, declarative way. Check The complete code is here.

 def on_mount(self) -> None:
       table = self.query_one(DataTable)
       table.add_columns("ID", "Product", "Category", "Price", "Sales", "Rating")
       table.cursor_type = "row"
       self.generate_sample_data(5)
       self.set_interval(0.1, self.update_progress)
  
   def generate_sample_data(self, count: int = 5) -> None:
       table = self.query_one(DataTable)
       categories = ["Electronics", "Books", "Clothing"]
       products = {
           "Electronics": ["Laptop", "Phone", "Tablet", "Headphones"],
           "Books": ["Novel", "Textbook", "Magazine", "Comic"],
           "Clothing": ["Shirt", "Pants", "Jacket", "Shoes"]
       }
      
       for _ in range(count):
           category = random.choice(categories)
           product = random.choice(productsArtificial Intelligence)
           row_id = self.total_rows + 1
           price = round(random.uniform(10, 500), 2)
           sales = random.randint(1, 100)
           rating = round(random.uniform(1, 5), 1)
          
           table.add_row(
               str(row_id),
               product,
               category,
               f"${price}",
               str(sales),
               str(rating)
           )
          
           self.total_rows += 1
           self.total_sales += sales
      
       self.update_stats()
  
   def update_stats(self) -> None:
       self.query_one("#card-rows", StatsCard).value = self.total_rows
       self.query_one("#card-sales", StatsCard).value = self.total_sales
      
       if self.total_rows > 0:
           table = self.query_one(DataTable)
           total_rating = sum(float(row[5]) for row in table.rows)
           self.avg_rating = round(total_rating / self.total_rows, 2)
           self.query_one("#card-rating", StatsCard).value = self.avg_rating
  
   def update_progress(self) -> None:
       progress = self.query_one(ProgressBar)
       progress.advance(1)
       if progress.progress >= 100:
           progress.progress = 0

We implemented all the logic to generate data, calculate statistics, animate progress, and update cards. We saw how quickly we can bind backend logic to frontend components using Textual’s reactive model. This step makes the dashboard feel alive as the numbers update immediately and the progress bar animates smoothly. Check The complete code is here.

 @on(Button.Pressed, "#btn-add")
   def handle_add_button(self) -> None:
       name_input = self.query_one("#input-name", Input)
       category = self.query_one("#select-category", Select).value
      
       if name_input.value and category:
           table = self.query_one(DataTable)
           row_id = self.total_rows + 1
           price = round(random.uniform(10, 500), 2)
           sales = random.randint(1, 100)
           rating = round(random.uniform(1, 5), 1)
          
           table.add_row(
               str(row_id),
               name_input.value,
               str(category),
               f"${price}",
               str(sales),
               str(rating)
           )
          
           self.total_rows += 1
           self.total_sales += sales
           self.update_stats()
           name_input.value = ""
  
   @on(Button.Pressed, "#btn-clear")
   def handle_clear_button(self) -> None:
       table = self.query_one(DataTable)
       table.clear()
       self.total_rows = 0
       self.total_sales = 0
       self.avg_rating = 0
       self.update_stats()
  
   @on(Button.Pressed, "#btn-generate")
   def handle_generate_button(self) -> None:
       self.generate_sample_data(10)
  
   def action_toggle_dark(self) -> None:
       self.dark = not self.dark
  
   def action_add_row(self) -> None:
       self.handle_add_button()
  
   def action_clear_table(self) -> None:
       self.handle_clear_button()




if __name__ == "__main__":
   import nest_asyncio
   nest_asyncio.apply()
   app = DataDashboard()
   app.run()

We use button handlers, keyboard shortcuts, and application-level functions to connect UI events to backend operations. When we run the application, we interact with a fully functional dashboard that responds instantly to every click and command. This code snippet completes the application and demonstrates how Textual easily helps us build dynamic, state-driven UIs.

In summary, we see the entire dashboard put together in a fully functional interactive form, running directly from the notebook environment. We experienced firsthand how Textual allowed us to design a terminal UI with the structure and feel of a web application, while entirely using Python. This tutorial gives us confidence that as we continue to experiment with Textual’s modern reactive UI capabilities, we can extend this foundation and even add charts, API feeds, and multi-page navigation.


Check The complete code is here. Please feel free to check out our GitHub page for tutorials, code, and notebooks. In addition, welcome to follow us twitter And don’t forget to join our 100k+ ML SubReddit and subscribe our newsletter. wait! Are you using Telegram? Now you can also join us via telegram.


Asif Razzaq is the CEO of Marktechpost Media Inc. As a visionary entrepreneur and engineer, Asif is committed to harnessing the potential of artificial intelligence for the benefit of society. His most recent endeavor is the launch of Marktechpost, an AI media platform that stands out for its in-depth coverage of machine learning and deep learning news that is technically sound and easy to understand for a broad audience. The platform has more than 2 million monthly views, which shows that it is very popular among viewers.

🙌 FOLLOW MARKTECHPOST: Add us as your go-to source on Google.

You may also like...