Custom sidebar#

A bespoke sidebar you build by hand — a faceted filter panel, a tool palette, a layer list. The escape hatch for when the sidebar isn’t navigation at all, so none of the built-in providers fit.

Custom filter sidebar — light theme Custom filter sidebar — dark theme

How it works#

panel() claims the sidebar as a blank container and returns it as a context manager — fill it with any widgets. You then drive the content area yourself through shell.content (also a container), typically by binding widgets to a Signal. Nothing is added or collapsed automatically — the panel is yours to fill and manage.

with shell.panel():
    bs.Label("Filters", font="heading-md")
    bs.SelectButton(options=["All", "Electronics", "Home"], signal=category)

with shell.content:
    bs.Label("Results", font="heading-lg")
    bs.Label(textsignal=results)

Because there are no pages, navigate() does not apply — the content is whatever you put there and update.

Example#

 1"""Custom sidebar — a bespoke sidebar the providers can't express (search filters).
 2
 3``panel()`` claims the sidebar as a blank container you fill with any widgets, and
 4you drive the content area yourself via ``shell.content``. A faceted filter
 5sidebar — category, price, rating — feeding a results area is the classic case:
 6it isn't navigation, so none of the nav providers fit. Reach for ``panel()`` only
 7when ``add_page`` / ``list_nav`` / ``tree_nav`` cannot express your sidebar.
 8"""
 9import bootstack as bs
10
11PRODUCTS = [
12    {"name": "Wireless Mouse", "category": "Electronics", "price": 24},
13    {"name": "Desk Lamp", "category": "Home", "price": 39},
14    {"name": "Mechanical Keyboard", "category": "Electronics", "price": 89},
15    {"name": "Throw Pillow", "category": "Home", "price": 19},
16    {"name": "USB-C Hub", "category": "Electronics", "price": 45},
17]
18
19with bs.AppShell(title="Shop", size=(900, 580)) as shell:
20    shell.commandbar.add_label("Shop", font="heading-md")
21    shell.commandbar.add_spacer()
22    shell.commandbar.add_button(icon="circle-half", on_click=bs.toggle_theme)
23
24    category = bs.Signal("All")
25    max_price = bs.Signal(100)
26    results = bs.Signal("")
27
28    def recompute(*_):
29        matches = [
30            p for p in PRODUCTS
31            if (category() == "All" or p["category"] == category())
32            and p["price"] <= max_price()
33        ]
34        lines = [f"{p['name']} — ${p['price']}" for p in matches]
35        results.set("\n".join(lines) if lines else "No products match.")
36
37    # A bespoke filter sidebar — not navigation, so panel() is the right tool.
38    with shell.panel():
39        with bs.VStack(fill="x", anchor_items="w", gap=12, padding=16):
40            bs.Label("Filters", font="heading-md")
41            bs.Label("Category", font="caption")
42            bs.SelectButton(options=["All", "Electronics", "Home"], signal=category)
43            bs.Label("Max price", font="caption")
44            bs.Slider(min_value=10, max_value=100, signal=max_price)
45
46    # Drive the content region by hand from the filter signals.
47    with shell.content:
48        with bs.VStack(fill="both", expand=True, anchor_items="w", gap=8, padding=20):
49            bs.Label("Results", font="heading-lg")
50            bs.Label(textsignal=results)
51
52    category.subscribe(recompute)
53    max_price.subscribe(recompute)
54    recompute()
55
56shell.run()

When to use#

Reach for a custom sidebar only when add_page, list_nav, and tree_nav genuinely can’t express what you need — a sidebar that isn’t a navigation list. If you want collapsible sections of navigation, prefer a grouped sidebar (or an Accordion inside the panel for collapsible content). For records, use a list or tree master–detail.