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.
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.