Layouts & error boundaries
Goal
Structure pages with VStack, HStack, Grid, Columns, Tabs,
Card, Sidebar, Form, and contain failures with ErrorBoundary.
Layout primitives
| Element | When to use |
|---|---|
VStack / HStack |
Vertical/horizontal sequences; HStack supports gap= |
Grid / Columns |
Responsive columns; Grid takes columns= count |
Tabs |
Pair (title, child) entries; each child is its own subtree |
Card |
Group related controls with a simple frame |
Sidebar |
Shell sidebar region when not using Streamlit multipage-only layouts |
Form |
Widgets that share submit semantics (form_key=) |
Spacer / Divider / Title / Subheader / Markdown |
Typography and rhythm |
Error boundaries
Wrap risky subtrees:
from streamtree import component
from streamtree.elements import ErrorBoundary, Markdown, Text, VStack
@component
def RiskyChartRegion():
return Text("replace with chart subtree")
@component
def SafeDashboard():
return ErrorBoundary(
child=RiskyChartRegion(),
fallback=VStack(Text("Chart unavailable"), Markdown("Try again later.")),
on_error=None, # optional logger
)
On failure the renderer logs and draws fallback (see renderer tests for coverage).
Composite example
The Phase 2 composite demo combines Routes, ErrorBoundary, app_context, and
submit_many:
"""Phase 2 composite: ``ErrorBoundary``, ``app_context``, ``Routes``, ``submit_many``.
Run: ``streamlit run examples/phase2_composite_demo.py``
Wraps the tree in :func:`streamtree.app_context.provider` so ``lookup`` works inside
components. ``submit_many`` jobs use stable keys; rerun to poll until both finish.
"""
from __future__ import annotations
from streamtree import component, render
from streamtree.app_context import lookup, provider
from streamtree.asyncio import submit_many
from streamtree.elements import (
Button,
ErrorBoundary,
Markdown,
Page,
Routes,
Text,
Title,
VStack,
)
from streamtree.routing import set_route
@component
def Boom() -> object:
"""Raises on purpose so ``ErrorBoundary`` can show its fallback."""
raise RuntimeError("demo failure")
@component
def ContextBadge() -> object:
return Text(f"app_label from context: {lookup('app_label', default='?')!r}")
@component
def ParallelStrip() -> object:
a, b = submit_many((("demo_a", lambda: 1), ("demo_b", lambda: 2)))
if a.status() == "done" and b.status() == "done":
return Text(f"submit_many done: {a.result()} + {b.result()} = 3")
return Text(f"Parallel jobs: {a.status()=!s} {b.status()=!s} (rerun to poll)")
@component
def App() -> object:
return Page(
VStack(
Title("Phase 2 composite"),
ErrorBoundary(
child=Boom(),
fallback=Markdown("**ErrorBoundary** caught a subtree failure."),
),
ContextBadge(),
Markdown("---"),
Button("Home", on_click=lambda: set_route("home")),
Button("Async strip", on_click=lambda: set_route("async")),
Routes(
routes=(
("home", Text("Use buttons above to switch ``route`` query param.")),
("async", ParallelStrip()),
),
default="home",
),
)
)
if __name__ == "__main__":
with provider(app_label="phase2-composite-demo"):
render(App())
See also
- Phase 2 portals & prefetch
- Phase 2 form layout — grid forms
- Examples —
phase2_layout_demo.py