Shell — Sidebar
Anatomy
Grouped navigation, a header slot, current-page indication and optional mobile tabs. Below 64rem the enhanced shell opens the same navigation inside a native modal drawer. Without JS, every navigation label remains visible above the page.
maud_ui::blocks::shell::sidebar
Props
Generated from public Rust fields. Defaults are evaluated from the implementation.
| Prop | Type | Default | Description |
|---|---|---|---|
state | crate::blocks::state::State | Ready | Explicit presentation state; Ready preserves the ordinary content. |
id | String | "mui-app" | Stable, unique ID: navigation, drawer and local preferences are scoped to it. |
brand | Markup | PreEscaped("<span class=\"mui-block--shell__brand-name\">App</span>") | No description |
brand_mark | Option<super::brand_mark::Props> | None | Typed identity shared with app_header; overrides legacy brand markup. |
header | Option<Markup> | None | No description |
sidebar_footer | Option<Markup> | None | No description |
nav_groups | Vec<NavGroup> | [] | No description |
active_path | String | "" | No description |
user | Option<UserBlock> | None | No description |
mobile_navigation | MobileNavigation | Drawer | No description |
mobile_tab_bar | Option<Markup> | None | Caller-owned schema tabs, using this shell's drawer ID. |
collapsible | bool | true | No description |
default_collapsed | bool | false | No description |
topbar_title | Option<String> | None | No description |
topbar_actions | Markup | PreEscaped("") | No description |
page_header | Option<page_header::Props> | None | Overrides the title/actions topbar; the shell supplies its own menu trigger. |
app_header | Markup | PreEscaped("") | No description |
app_footer | Markup | PreEscaped("") | No description |
embedded | bool | false | Use a section for the content when composing inside an existing main landmark. |
children | Markup | PreEscaped("") | No description |
Examples
light · optional header and footer
Settings
Reservations
4 reservations · Tuesday, 8 September
| Guest | Room | Status | Nights | |
|---|---|---|---|---|
| Amira KhanRS-2048 | Garden suite | Arriving | 4 | View |
| Theo MartinRS-2049 | Courtyard room | Arriving | 2 | View |
| Lina ChenRS-2046 | Terrace suite | Checked in | 3 | View |
| Jonas NielsenRS-2050 | Garden room | Needs review | 5 | View |
No guests match. Try another name or choose All.
Amira Khan
RS-2048 · Garden suite · 8–12 September
A little context, right where the next decision happens.
Plan a stay
Add a reservation to this example.
Prepare arrivals
See who's arriving and their room.
Build your workspace
Use this shell in your own app.
Example data · Changes stay on this page.
dark · optional header and footer
Settings
Reservations
4 reservations · Tuesday, 8 September
| Guest | Room | Status | Nights | |
|---|---|---|---|---|
| Amira KhanRS-2048 | Garden suite | Arriving | 4 | View |
| Theo MartinRS-2049 | Courtyard room | Arriving | 2 | View |
| Lina ChenRS-2046 | Terrace suite | Checked in | 3 | View |
| Jonas NielsenRS-2050 | Garden room | Needs review | 5 | View |
No guests match. Try another name or choose All.
Amira Khan
RS-2048 · Garden suite · 8–12 September
A little context, right where the next decision happens.
Plan a stay
Add a reservation to this example.
Prepare arrivals
See who's arriving and their room.
Build your workspace
Use this shell in your own app.
Example data · Changes stay on this page.
Loading, empty, error and disabled
Each example uses the block’s state prop. Empty and error are separate outcomes.
Usage and composition
Import and example
use maud::html;
use maud_ui::blocks::shell::sidebar::{self, MobileNavigation, NavGroup, NavItem, Props};
sidebar::render(Props {
id: "reservation-app".into(),
brand: html! { "Garden House" },
header: Some(html! { label { "Workspace" select { option { "Front desk" } } } }),
nav_groups: vec![NavGroup {
label: Some("Operations".into()),
items: vec![NavItem { label: "Reservations".into(), short_label: Some("Stays".into()), href: "/reservations".into(), icon: None, badge: Some("48".into()), ..Default::default() }],
}],
active_path: "/reservations".into(),
mobile_navigation: MobileNavigation::Tabs,
children: html! { p { "Your page content" } },
..Default::default()
});
NavItem fields: label/href: String, short_label: Option..Default::default() for optional fields. Nested items indent without a rail. A group's label names its accessible group. Tabs uses the first four flattened destinations plus More; any later current destination marks More as current. All destinations remain in the drawer. Put your frequent destinations first. The optional header can use a labelled input with data-mui-nav-search for local destination filtering.
Navigation preferences
Labeled groups use native details/summary and remember their state under mui-nav-group:{navigation-id}:{index}:{label}. A current destination opens its group on route entry unless the user has explicitly saved it closed. Sidebar collapse uses mui-shell-rail:{id}; storage failures fall back to rendered defaults. Keep IDs stable and unique, and keep group ordering stable to retain preferences. The desktop icon rail appears at 64rem, with full accessible labels and title hints. It temporarily opens groups to retain every icon and restores their remembered state when expanded or moved into the phone drawer. Inputs in the header and arbitrary footer controls are hidden while collapsed; place essentials in the page bar as well.
Header and footer composition
Pass app_header::render(...) and app_footer::render(...) into the corresponding shell slots. Their empty defaults emit no wrappers. Pass page_header::Props for breadcrumbs, native search or a custom command trigger, actions and switchers. The shell page renders this full composition in both themes. The landing example supports client-side guest filtering, row selection and adding a local example reservation; its data is fictional and never persisted or sent to a booking system.
Below 64rem, a supplied page header moves its original breadcrumb and actions/switchers into the drawer. The page bar keeps the menu, current title and search icon. At wider sizes controls return to the header; the native search input is visible. Without enhancement, the native navigation details and Settings disclosure retain access to every destination and control. The shared example uses “Reservations” and one guest-search field; the header shortcut focuses that field, and the sidebar contains no duplicate search.
Presentation states (0.10.1)
Props::state: maud_ui::blocks::state::State defaults to Ready. Loading { message } shows skeletons; Empty { message, action } and Error { message, retry } provide distinct recovery paths; Disabled { reason } retains admitted content in an inert subtree with an external reason. Loading, empty and error omit ready content. Inert disables interaction, not server authorization or submission of values by an enclosing form. Each live API page shows all four states together. Add state: Default::default() to exhaustive Props literals.
Typed identity (0.11.0)
brand_mark: Option<shell::brand_mark::Props> defaults to None and overrides the raw brand slot when supplied. The mark contains a logo, wordmark and optional tagline. Add the new field to exhaustive Props literals. See brand mark.
State::Absent (0.11.0) means no input/rule was declared and emits nothing. Keep any caller-owned section heading inside the same conditional. Use Error only for a declared operation that failed; never show an unconfigured-rule message to the user.
Still frame and current navigation (0.12.0)
Density never moves the frame. The root shell uses min-height: 100dvh and rows auto 1fr auto. Its .mui-block--shell__body contains a stretching .mui-block--shell__sidebar-column and the existing main region. Background and hairline belong to the column; the original sidebar is the sticky, independently scrolling inner node. Drawer close restores that node into its column. Embedded previews deliberately keep a 32rem minimum. See shell frame for all --mui-shell-* tokens and short/long fixtures.
The current item has a flat tint, zero corner radius and a 2px accent bar at the column edge. Nested labels indent inside the row so the bar stays aligned. Exactly one item retains aria-current="page". Its group gets data-contains-current="true"; the parent label uses --mui-text-primary, weight 600, and an accent chevron, with no fill. Group collapse does not erase this state. The primitive sidebar uses the same presentation.
Content is a stack with one --mui-stack-gap (16px) and no added child block margins. Use .mui-page-stack inside caller-owned wrappers. Put per-page density on that content container, or retain the document setting; shell spacing and type stay fixed. Existing Rust Props remain compatible. Custom direct-child CSS for the old sidebar position must target the new body/column; update CSS and JS together.
Review short and long fixtures after running cargo run --example frame_fixture.
Accessibility
Native page links and aria-current, labelled groups, visible mobile labels, safe-area clearance and focus rings. More/Menu have an ordinary fallback href before enhancement. The drawer moves the existing sidebar node without duplicating controls or IDs; native dialog provides Escape and focus containment. Closing restores navigation and trigger focus. Returning to desktop closes the drawer. Server swaps reinitialize through MaudUI's existing lifecycle. Use a fresh unique id for each shell. Use embedded: true when nesting inside an existing main landmark. Embedded mobile tabs flow below the example; ordinary app tabs remain fixed.
Use short_label: Some("Stays".into()) to fit a long label such as Reservations in a bottom tab. Use None to display the full label. Empty or whitespace-only short labels fall back to the full label. Labels stay on one line and ellipsize; five destinations use a smaller type size. The accessible name includes both the visible short label and the full label, so voice control and screen readers retain context. Sidebar labels ellipsize on desktop with their full text available in a title hint and accessible name; count badges never shrink. The drawer wraps labels.