AppShell#
A full application scaffold: a menu / command bar across the top, a navigation sidebar on the left, and a content area that swaps as you navigate. With a single set of pages it is a plain sidebar app; add more than one workspace and a VS Code-style icon rail appears to switch between them. A full-width status band can run along the bottom.
Usage#
Static pages#
add_page(key, text=, icon=) registers a sidebar nav item and its content page
together. Use the returned value as a context manager to place child widgets on
that page. navigate() selects the active page (the sidebar selection follows).
with bs.AppShell(title="My App") as shell:
with shell.add_page("dashboard", text="Dashboard", icon="house"):
bs.Label("Dashboard content")
with shell.add_page("settings", text="Settings", icon="gear"):
bs.Label("Settings content")
shell.navigate("dashboard")
shell.run()
Scrollable pages#
Pass scrollable=True to wrap a page’s content in a vertical scroll area.
with shell.add_page("log", text="Log", icon="list", scrollable=True):
for i in range(100):
bs.Label(f"Log entry {i}")
Workspaces (the rail)#
Add named workspaces with add_workspace() — each gets its own rail icon and
its own sidebar, authored with the very same page API. The rail appears
automatically once there is more than one workspace, so single-tier and two-tier
apps are written the same way. add_footer_workspace() pins a workspace (e.g.
Settings) to the bottom of the rail. Shell-level page methods and
add_workspace() are mutually exclusive — use one style or the other.
with bs.AppShell(title="Console", size=(960, 600)) as shell:
with shell.add_workspace("acquire", text="Acquire", icon="cpu") as ws:
with ws.add_page("sensors", text="Sensors", icon="thermometer-half"):
bs.Label("Sensors", font="heading-lg")
ws.add_header("Hardware")
with ws.add_page("ports", text="Ports", icon="usb-symbol"):
bs.Label("Ports", font="heading-lg")
with shell.add_workspace("devices", text="Devices", icon="hdd-stack") as ws:
ws.list_nav(devices)
@ws.detail
def show(record):
bs.Label(record["title"], font="heading-lg")
with shell.add_footer_workspace("settings", text="Settings", icon="gear") as ws:
with ws.add_page("general", text="General", icon="sliders"):
bs.Label("Settings", font="heading-lg")
shell.navigate("acquire", "sensors") # workspace first, then page
shell.run()
Custom panel#
panel() claims the sidebar as a blank container you fill yourself — the escape
hatch when none of the providers fit. Drive the content region with
shell.content (or a workspace’s ws.content).
with shell.panel():
bs.Label("Filters", font="heading-md")
with bs.Accordion():
...
Command bar#
shell.commandbar is the built-in CommandBar,
in the top chrome row. Add buttons, labels, separators, and an add_spacer()
to push trailing items to the right.
shell.commandbar.add_spacer()
shell.commandbar.add_button(icon="circle-half", on_click=bs.toggle_theme)
shell.commandbar.add_button(label="Save", icon="save", on_click=save)
Status bar#
shell.statusbar is a full-width band along the bottom, intended for
passive status — counts, sync state, a ready message. Interactive controls
(buttons, a search box) belong on the command bar by convention; the status bar
reads best as a quiet display strip. It renders only once a segment is added, or
when the shell is built with show_statusbar=True. add_spacer() (or
side="right") pushes following segments to the right cluster.
shell.statusbar.add_text("Ready")
shell.statusbar.add_spacer()
shell.statusbar.add_text("v1.0", side="right")
Pass textsignal= to make a segment reactive — bind it to a
Signal and it updates live as the value changes:
selected = bs.Signal("0 selected")
shell.statusbar.add_text(textsignal=selected)
...
selected.set("3 selected") # the status updates automatically
The StatusBar handle also supports
add_widget() (a custom passive widget), add_spacer(), and clear();
or use it as a container — with shell.statusbar: parents widgets into the
left cluster.
Styling#
Each region’s background is a surface token you can
override; the defaults give the shell its layered look (the rail and status band
sit on the elevated chrome surface, the sidebar a step below). The dividers
and the nav-item selection wash blend against these automatically.
Kwarg |
Default |
Region |
|---|---|---|
|
|
The top menu / command-bar band. |
|
|
The workspace rail. |
|
|
The navigation sidebar. |
|
|
The bottom status band. |
The selected nav item is neutral by default. Set nav_accent to tint the
selection (and the rail’s indicator) with an accent, and nav_selection to
choose how the accent reads:
# subtle accent wash + accent text (the default emphasis)
bs.AppShell(nav_accent="primary")
# a filled accent pill with on-accent (white) text — higher emphasis
bs.AppShell(nav_accent="primary", nav_selection="solid")
nav_accent colors the rail indicator bar, the static nav pills/rows, and the
list_nav / tree_nav selection wash; nav_selection ('ghost' default
or 'solid') applies to the static nav items.
Events#
All shorthands take a handler (returns a cancellable
Subscription) or no argument (returns a
composable Stream).
Shorthand |
Handler receives |
|---|---|
|
|
|
|
|
|
|
shell.on_page_change(lambda e: print("now on:", e.page))
shell.on_workspace_change().listen(lambda e: print("workspace:", e.workspace))
Theme, locale, and configuration#
Like App, an AppShell is configured
through flat constructor keyword arguments, and the same options are read and
changed at runtime through shell.* properties. Assigning shell.theme or
shell.locale takes effect live.
shell = bs.AppShell(
title="My App",
theme="bootstrap-dark",
light_theme="nord-light",
dark_theme="nord-dark",
locale="de_DE",
)
shell.theme = "bootstrap-light" # switch the theme now
React to changes and persist them across launches with a Store — from_store() restores configuration and tolerates
version skew, and the change events write each value back:
from bootstack.store import Store
store = Store("settings")
shell = bs.AppShell.from_store(store, title="My App")
shell.on_theme_change(lambda theme: store.update(theme=theme))
See App Configuration for the full configuration reference — every option, the locale-derived read-only properties, and window-state persistence.
Window options#
bs.AppShell(
title="My App",
icon="assets/app.ico", # icon file, an Image, or an AppIcon
size=(1024, 768),
min_size=(640, 480),
resizable=(True, True),
)
# Custom chrome (no OS title bar; draws a themed border instead)
bs.AppShell(undecorated=True)
See also#
PageStack —
page navigation without a built-in sidebar.
Tabs —
tab-strip navigation.
CommandBar —
the standalone command-bar widget.
API#
The complete reference for AppShell lives on the
Application API page. At a glance:
Application window with rail + swappable sidebar navigation and content. |
Full Example#
1
2with bs.AppShell(title="My App", size=(800, 540)) as shell:
3
4 # ── Toolbar ───────────────────────────────────────────────────────────────
5 shell.commandbar.add_spacer()
6 shell.commandbar.add_button(icon="circle-half", on_click=bs.toggle_theme)
7
8 # ── Pages ─────────────────────────────────────────────────────────────────
9 with shell.add_page("dashboard", text="Dashboard", icon="speedometer2"):
10 with bs.VStack(fill="x", gap=12, padding=24):
11 bs.Label("Dashboard", font="heading-lg")
12 bs.Label("Welcome back. Here is your overview.")
13 with bs.Grid(columns=3, gap=12, fill="x", sticky_items="ew"):
14 with bs.Card(padding=16, gap=4):
15 bs.Label("Revenue", font="caption")
16 bs.Label("$12,400", font="heading-md")
17 with bs.Card(padding=16, gap=4):
18 bs.Label("Users", font="caption")
19 bs.Label("1,280", font="heading-md")
20 with bs.Card(padding=16, gap=4):
21 bs.Label("Orders", font="caption")
22 bs.Label("340", font="heading-md")
23
24 with shell.add_page("inbox", text="Inbox", icon="inbox"):
25 with bs.VStack(fill="x", gap=8, padding=24):
26 bs.Label("Inbox", font="heading-lg")
27 bs.Label("No new messages.")
28
29 shell.add_separator()
30 shell.add_header("Documents")
31
32 with shell.add_page("files", text="Files", icon="folder"):
33 with bs.VStack(fill="x", gap=8, padding=24):
34 bs.Label("Files", font="heading-lg")
35 bs.Label("Your documents will appear here.")
36
37 with shell.add_page("images", text="Images", icon="image"):
38 with bs.VStack(fill="x", gap=8, padding=24):
39 bs.Label("Images", font="heading-lg")
40 bs.Label("Your images will appear here.")
41
42 with shell.add_footer_page("settings", text="Settings", icon="gear"):
43 with bs.VStack(fill="x", gap=8, padding=24):
44 bs.Label("Settings", font="heading-lg")
45 bs.Label("Adjust your preferences.")
46
47 shell.navigate("dashboard")
48
49shell.run()