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.

AppShell demo — light theme AppShell demo — dark theme

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}")

Data-bound sidebar (master–detail)#

Instead of authored pages, fill the sidebar straight from a data source with list_nav() (a flat list) or tree_nav() (a hierarchy). Decorate a builder with @shell.detail to render the body for the selected record — it receives the record as a dict. The first item is selected automatically.

from bootstack.data import MemoryDataSource

devices = MemoryDataSource().load([
    {"id": 1, "title": "Sensor A", "text": "online"},
    {"id": 2, "title": "Sensor B", "text": "offline"},
])

with bs.AppShell(title="Devices") as shell:
    shell.list_nav(devices)

    @shell.detail
    def show(record):
        with bs.VStack(fill="both", gap=12, padding=24):
            bs.Label(record["title"], font="heading-lg")
            bs.Label(record["text"])
shell.run()

A workspace is filled by exactly one provider — static add_page or list_nav or tree_nav or a custom panel(). Mixing them raises.

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

chrome_surface

'chrome'

The top menu / command-bar band.

rail_surface

'chrome'

The workspace rail.

sidebar_surface

'raised'

The navigation sidebar.

statusbar_surface

'chrome'

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

on_page_change

PageChangeEvent

on_workspace_change

WorkspaceChangeEvent

on_sidebar_toggle

PaneToggleEvent

on_sidebar_mode_change

DisplayModeEvent

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 Storefrom_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:

AppShell

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()