How to Design a Fully Interactive, Reactive, and Dynamic Terminal-Based Data Dashboard Using Textual?


In this tutorial, we build an advanced interactive dashboard using Textual, and we explore how terminal-first UI frameworks can feel as expressive and dynamic as modern web dashboards. As we write and run each snippet, we actively construct the interface piece by piece, widgets, layouts, reactive state, and event flows, so we can see how Textual behaves like a live UI engine right inside Google Colab. By the end, we notice how naturally we can blend tables, trees, forms, and progress indicators into a cohesive application that feels fast, clean, and responsive. Check out the FULL CODES 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 Textual application. As we define the StatsCard widget, we establish a reusable component that reacts to changes in value and updates itself automatically. We begin to see how Textual’s reactive system lets us create dynamic UI elements with minimal effort. Check out the FULL CODES 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, key bindings, and reactive attributes. We decide how the app should look and behave right from the top, giving us full control over themes and interactivity. This structure helps us create a polished dashboard without writing any HTML or JS. Check out the FULL CODES 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 compose the entire UI layout, arranging containers, cards, form inputs, buttons, a navigation tree, and a data table. As we structure these components, we watch the interface take shape exactly the way we envision it. This snippet lets us design the visual skeleton of the dashboard in a clean, declarative manner. Check out the FULL CODES 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 implement all the logic for generating data, computing statistics, animating progress, and updating cards. We see how quickly we can bind backend logic to frontend components using Textual’s reactive model. This step makes the dashboard feel alive as numbers update instantly and progress bars animate smoothly. Check out the FULL CODES 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 connect UI events to backend actions using button handlers, keyboard shortcuts, and app-level functions. As we run the app, we interact with a fully functional dashboard that responds instantly to every click and command. This snippet completes the application and demonstrates how easily Textual enables us to build dynamic, state-driven UIs.

In conclusion, we see the whole dashboard come together in a fully functional, interactive form that runs directly from a notebook environment. We experience firsthand how Textual lets us design terminal UIs with the structure and feel of web apps, while staying entirely in Python. This tutorial leaves us confident that we can extend this foundation, even adding charts, API feeds, and multi-page navigation, as we continue to experiment with Textual’s modern reactive UI capabilities.


Check out the FULL CODES here. Feel free to check out our GitHub Page for Tutorials, Codes and Notebooks. Also, feel free to follow us on Twitter and don’t forget to join our 100k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.


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 social good. His most recent endeavor is the launch of an Artificial Intelligence Media Platform, Marktechpost, which stands out for its in-depth coverage of machine learning and deep learning news that is both technically sound and easily understandable by a wide audience. The platform boasts of over 2 million monthly views, illustrating its popularity among audiences.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *