AI

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

In this tutorial, we have built an advanced interactive dashboard using Textualwe explore how early end user interface frameworks can be as expressive and dynamic as modern web dashboards. As we write and run each snippet, we effectively build the interface piece by piece, widgets, layouts, interactive state, and event streams, so we can see how Textual behaves like a live UI engine right inside Google Colab. Ultimately, we notice how naturally we can blend tables, trees, forms, and progress indicators into a cohesive application that feels fast, clean, and responsive. verify 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 create our script application. When we define the StatsCard widget, we create a reusable component that reacts to changes in the value and updates itself automatically. We begin to see how Textual’s interactive system allows us to create dynamic UI elements with minimal effort. verify 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 a DataDashboard class and configure global styles, keybindings, and interactive attributes. We decide how the app should look and behave properly 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. verify 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 write the entire UI layout, arrangement of containers, cards, form inputs, buttons, navigation tree, and spreadsheet. As we build these components, we watch the interface take shape exactly the way we envision it. This snippet allows us to design the visual structure of the dashboard in a clear and identifiable way. verify 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 to generate data, compute statistics, trigger progress, and update cards. We see how quickly we can connect backend logic to frontend components using the interactive Textual form. This step makes the dashboard look alive as the numbers update instantly and the progress bars move smoothly. verify 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 associate UI events with backend actions using button handlers, keyboard shortcuts, and application-level functions. While running the application, we interact with a fully-functional control panel that instantly responds to our every click and command. This snippet completes the application and demonstrates how easily Textual enables us to create dynamic, state-driven user interfaces.

In conclusion, we see the entire dashboard brought together in a fully functional interactive form that runs directly from the laptop environment. We experience first-hand how Textual allows us to design end-to-end user interfaces with the structure and look of web applications, while remaining 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 interactive UI capabilities.


verify Full codes here. Feel free to check out our website GitHub page for tutorials, codes, and notebooks. Also, feel free to follow us on twitter Don’t forget to join us 100k+ mil SubReddit And subscribe to Our newsletter. I am waiting! Are you on telegram? Now you can join us on Telegram too.


Asif Razzaq is the CEO of Marktechpost Media Inc. As a visionary entrepreneur and engineer, Asif is committed to harnessing the potential of AI for social good. His most recent endeavor is the launch of the AI ​​media platform, Marktechpost, which features in-depth coverage of machine learning and deep learning news that is technically sound and easy to understand by a broad audience. The platform has more than 2 million views per month, which shows its popularity among the masses.

🙌 FOLLOW MARKTECHPOST: Add us as a favorite source on Google.

Don’t miss more hot News like this! Click here to discover the latest in AI news!

2025-11-15 08:48:00

Related Articles

Back to top button