Skip to content

App shell ​

Three components frame a dashboard: SAppBar is the bar along the top, SSidebar is the navigation column that collapses to a rail and turns into a drawer on narrow screens, and SPage is the content area that sits below the bar and beside the column instead of underneath them.

They are a frame, not a layout engine. Rows, columns, gutters and breakpoint visibility stay with the grid utilities and .s-container; the shell only decides what the page has to reserve. The three are siblings inside the app container — the .s-root--app wrapper described in Isolation:

vue
<template>
  <div class="s-root s-root--app">
    <SAppBar>
      <template #prepend>
        <SButton
          variant="ghost"
          icon-only
          icon="menu"
          aria-label="Open navigation"
          @click="open = true"
        />
      </template>
      Dashboard
    </SAppBar>

    <SSidebar
      v-model:open="open"
      v-model:collapsed="collapsed"
      aria-label="Main"
    >
      …
    </SSidebar>

    <SPage>…</SPage>
  </div>
</template>

Order matters: the page reserves space with CSS that reads the bar and the column as preceding siblings. Nothing is measured in JavaScript — the bar publishes --s-app-bar-height, the sidebar publishes --s-sidebar-width and --s-sidebar-collapsed-width, and the page turns them into padding. A custom size is mirrored onto the wrapper element, so the neighbours inherit it.

Landmarks ​

The bar renders role="banner", the sidebar wraps its navigation in role="navigation" and the page renders role="main", all on plain div elements rather than header/nav/main: host stylesheets target those tags, and library markup stays out of their reach. Assistive technology sees the same regions either way.

A document has one banner and one main, so a second bar or a second page area sets :landmark="false". Give every sidebar an aria-label — a page with more than one navigation region needs the names to tell them apart.

A full shell ​

The bar sticks to the top and keeps its place in the flow, so the page needs no offset for it. The column is pinned beside the page. Every example below lives in a wrapper with a transform, which makes that wrapper the containing block for the pinned elements — otherwise they would escape into the documentation page.

Acme
Pick a report on the left.

The collapsed rail ​

v-model:collapsed narrows the column to collapsedWidth, and the page follows. A navigation built from SList needs nothing else: in the rail a row drops its text and its side padding, so the icon is centred and keeps the whole column as its hit target. For markup of your own, .s-sidebar__label marks the text that goes away, and anything that needs more than a hidden label reads the collapsed slot prop. The column never scrolls sideways, however wide the content is. The toggle's accessible name comes from the locale dictionary (expandSidebar / collapseSidebar); replace it per instance with expandLabel and collapseLabel, or everywhere through i18n.

side="end" puts the column on the trailing edge — the right side in a left-to-right document and the left one in RTL.

The rail keeps the icons and hands the width back to the page.

The drawer breakpoint ​

Below breakpoint the column becomes a drawer opened with v-model:open — the same content, a Drawer around it, focus trapped inside. The breakpoint is a name from the grid scale: sm, md (the default), lg or xl. The server renders the column and swaps to the drawer once the media query has been read in the browser, so nothing branches on a measured window width.

The example below asks for breakpoint="xl", so the narrow rendering shows on a wide screen too.

A page with a container ​

container wraps the content in the .s-container utility — centred, with the readable maximum of the current breakpoint and the container's own side padding. maxWidth caps it further, with or without the container, and :padded="false" hands the padding back to the content.

position="fixed" takes the bar out of the flow; the page then reserves its height, so a scrolling page passes under a bar that stays put. elevate-on-scroll gives that bar a shadow as soon as the window moves.

A container keeps long-form content within a readable measure while the page itself still spans the shell.

API ​

SAppBar ​

Props

NameTypeDefaultDescription
position | "sticky" | "static" | "fixed""sticky"How the bar is anchored: static scrolls away with the page, sticky stays at the top of the viewport while keeping its place in the flow, fixed leaves the flow entirely (the page then reserves the height by itself).
heightstring | numberundefinedBar height: a CSS length or a number of pixels. The value is published as --s-app-bar-height on the shell, so the page and the sidebar offset by it without measuring anything. 3.5rem by default.
borderedbooleantrueHairline along the bottom edge.
elevate-on-scrollbooleanfalseRaises the bar with a shadow once the window is scrolled — the Material behaviour for a bar that overlaps content. The elevation prop wins when it is set, giving a constant shadow.
colorstringundefinedSurface color: a name from the palette (primary/teal/teal-10). Replaces the default surface.
text-colorstringundefinedContent color on the fill: a name from the palette. White by default (set it for light color values).
flatbooleanundefinedRemoves the shadow. Overridden by the elevation prop when it is set.
elevation0 | 3 | 1 | 2 | 4 | 5undefinedShadow level 0–5 (scale); 0 means no shadow. Overrides flat.
landmarkbooleantrueMarks the bar as the page's banner landmark. Turn it off for a bar that is not the document's own header — a bar inside a panel, or a second bar on the same page.

Slots

NameDescription
prependLeading area: a menu button, a logo, a back arrow.
defaultCentre of the bar: the title, a search field, tabs.
appendTrailing area: actions, a notifications button, an avatar.

SSidebar ​

Props

NameTypeDefaultDescription
side"start" | "end""start"Edge the sidebar sits on, in logical terms: start is the left side in a left-to-right document and the right one in RTL.
widthstring | numberundefinedWidth of the expanded column: a CSS length or a number of pixels. 16rem by default. The value is published as --s-sidebar-width on the shell, so the page reserves the same width without measuring anything.
collapsed-widthstring | numberundefinedWidth of the collapsed rail: a CSS length or a number of pixels. 4rem by default, published as --s-sidebar-collapsed-width.
breakpoint"md" | "sm" | "lg" | "xl""md"Breakpoint at which the column appears: below it the sidebar is a drawer opened through v-model:open. A name from the grid scale — sm (600px), md (1024px), lg (1440px) or xl (1920px).
borderedbooleantrueHairline between the column and the page.
collapsiblebooleantrueShows the button that collapses the column to a rail of icons. Turning it off leaves v-model:collapsed under the application's own control.
aria-labelstringundefinedAccessible name of the navigation landmark. Set it whenever a page holds more than one navigation region, so screen readers can tell them apart.
expand-labelstringundefinedAccessible name of the toggle while the rail is collapsed (taken from the locale dictionary by default).
collapse-labelstringundefinedAccessible name of the toggle while the column is expanded (taken from the locale dictionary by default).
openbooleanfalseWhether the drawer is open below the breakpoint. Two-way binding via v-model:open.
collapsedbooleanfalseWhether the wide-screen column is collapsed to a rail of icons. Bound via v-model:collapsed.

Events

NameSignatureDescription
update:open(value: boolean) => voidEmitted when open changes. Used for two-way binding (v-model:open).
update:collapsed(value: boolean) => voidEmitted when collapsed changes. Used for two-way binding (v-model:collapsed).

Slots

NameDescription
headerTop of the sidebar: a logo, a workspace switcher.
defaultNavigation content. Receives the rail state so items can hide their labels.
footerBottom of the sidebar: a user card, secondary links.
toggleReplaces the collapse button; receives the rail state and the toggle handler.

SPage ​

Props

NameTypeDefaultDescription
paddedbooleantrueInner padding around the content.
containerbooleanfalseWraps the content in the .s-container utility: centred and limited to the readable width of the current breakpoint.
max-widthstring | numberundefinedMaximum content width: a CSS length or a number of pixels. The content stays centred, with or without container.
landmarkbooleantrueMarks the area as the page's main landmark. Turn it off where the page is not the document's main content — a preview inside a panel, or a second page area.

Slots

NameDescription
defaultPage content.