Recipe
The recipe is where you define your UI. It’s a method on your TextualApp subclass that receives a page — a BuilderBag with Textual widget definitions.
Basic Structure
class MyApp(TextualApp):
def recipe(self, page):
page.header()
page.static("Content goes here")
page.footer()
Every builder call adds a node to the source Bag. The compiler expands it, the renderer mounts it as a Textual widget.
Containers and Layout
Containers return a reference for nesting:
def recipe(self, page):
main = page.horizontal()
left = main.vertical()
left.static("Left panel")
right = main.vertical()
right.static("Right panel")
Available containers: vertical, horizontal, grid, center, middle, verticalscroll, horizontalscroll, scrollablecontainer.
Widgets
Leaf widgets don’t have children:
page.static("Display text")
page.input(value="^form.name", placeholder="Name")
page.button("Click me", variant="primary")
page.checkbox("Enable", value=True)
page.switch(value=False)
CSS
Inline stylesheets in the recipe:
page.css("""
#sidebar { width: 30; background: $surface; }
.highlight { color: green; text-style: bold; }
""")
CSS variables ($surface, $primary, etc.) work in page.css().
Key Bindings
page.binding(key="q", action="quit", description="Quit")
page.binding(key="f12", action="toggle_drawer", description="Inspector")
Bindings appear in the Footer widget and are clickable.
Style Attributes
CSS properties can be set directly on widgets:
page.vertical(id="drawer", width="^_system.drawer.width", display="^_system.drawer.display")
These are applied to widget.styles at mount time and are bindable with ^pointer.
Components
Reusable UI patterns defined with @component:
page.fieldset(title="User Info")
form = page.form(title="Settings")
form.input(placeholder="Name")
form.checkbox("Active")
Tabs
tabs = page.tabbedcontent(initial="main")
tab1 = tabs.tabpane(title="Main", id="main")
tab1.static("Main content")
tab2 = tabs.tabpane(title="Settings", id="settings")
tab2.input(placeholder="Config")
DataTable
dt = page.datatable()
dt.column(label="Name")
dt.column(label="Age")
dt.row("Alice", "30")
dt.row("Bob", "25")
Tree with Store
A Tree widget can display a Bag structure:
page.tree(label="data", store=self.data)
The tree populates recursively from the Bag and updates when the Bag changes.