Layout
App Shell
The frame a dashboard sits in: a sidebar that stays put and one column that scrolls.
import { AppShell } from 'omaris' Examples
Dashboard frame
The layout most omaris apps end up with. Only the content column scrolls — the sidebar stays put and the app bar finds its own scroller, which is what makes sticky work without a second scrollbar.
Overview
<script lang="ts"> import { AppShell, AppShellSidebar, AppShellMain, AppShellContent, NavigationDrawer, NavigationDrawerItem, TopAppBar } from 'omaris'; let selected = $state('Overview');</script><div class="h-96 w-full overflow-hidden rounded-shape-lg border border-border"> <AppShell class="h-full"> <AppShellSidebar breakpoint="sm"> <NavigationDrawer bordered label="Sections"> <NavigationDrawerItem label="Overview" selected={selected === 'Overview'} onclick={() => (selected = 'Overview')} /> <NavigationDrawerItem label="Orders" selected={selected === 'Orders'} onclick={() => (selected = 'Orders')} /> <NavigationDrawerItem label="Customers" selected={selected === 'Customers'} onclick={() => (selected = 'Customers')} /> </NavigationDrawer> </AppShellSidebar> <AppShellMain> <TopAppBar title={selected} sticky /> <AppShellContent width="lg"> <div class="flex flex-col gap-3"> <!-- Enough rows to have something to scroll. --> {#each { length: 12 }, i (i)} <div class="h-12 rounded-shape-md bg-surface-container"></div> {/each} </div> </AppShellContent> </AppShellMain> </AppShell></div> Content width
width sets the reading measure and centres it. full is right for a table, md for prose.
width="sm" — a column narrow enough to read comfortably, centred in whatever space the shell has.
<script lang="ts"> import { AppShell, AppShellMain, AppShellContent } from 'omaris';</script><div class="h-64 w-full overflow-hidden rounded-shape-lg border border-border"> <AppShell> <AppShellMain> <AppShellContent width="sm"> <div class="rounded-shape-md bg-primary-container p-4"> <p class="text-body-md text-primary-container-foreground"> width="sm" — a column narrow enough to read comfortably, centred in whatever space the shell has. </p> </div> </AppShellContent> </AppShellMain> </AppShell></div> When to use it
Use it for
- The frame of a dashboard.
AppShellSidebarholds aNavigationDrawer,AppShellMainholds aTopAppBarandAppShellContent. Only the content column scrolls; the sidebar and bar stay put with nostickyanywhere. - An inspector on the trailing edge, like the selected row's details. Add a second
AppShellSidebar side="end". - A bar a selection needs, or a form's unsaved-changes strip.
AppShellFooter stickyholds the bottom of the column. - A page's reading measure.
AppShellContent widthdefaults tofull, which a table wants;mdfor prose,smfor a lone form.
Not for
- A marketing or landing page → a plain layout. The document should scroll.
- A sign-in screen → a centred Card on the page.
- Panes the person drags → Resizable inside
AppShellMain. The shell's sidebar is a fixed column. - Long articles where the address bar should collapse on a phone →
scroll="page", which scrolls the document and sticks the sidebar instead.
Do
- Put it in
+layout.svelteonce. Pages render insideAppShellContent. - Plan the phone. The sidebar column is gone below
breakpoint(mdby default), so a phone needs a modal Navigation Drawer rendered outside the sidebar, or a Navigation Bar in the main column. - Use
padded={false}when a page brings its own edges, like a full-bleed map or a kanban board. - Use
AppShellHeaderfor a bar plus a filter row that must stay up together. ATopAppBaron its own sticks by itself and finds the scroller from the shell.
Don't
- Put
h-screenoroverflow-autoon your own wrappers inside it. The shell already picked the scrolling element; a second one brings back the double scrollbar. - Pass
scrollContainerto aTopAppBarinside the shell. It already knows. - Nest an
AppShellper route. - Leave
width="full"on a page of prose. Lines a screen wide do not read.
Quick reference
width smmdlgxlfull(default)
The measure. full fills the column — right for a wide table.
API
AppShell
The frame a dashboard is built in.
The thing that goes wrong in a hand-rolled dashboard is always the same: the whole document scrolls, so the sidebar — which is only as tall as the viewport — slides up out of the window with everything else. The fix is to decide once which element scrolls, and this is where that decision lives:
- scroll="main" (the default) pins the shell to the viewport and lets only the content column scroll. The sidebar and the app bar stay exactly where they are, forever, with no sticky anywhere. - scroll="page" lets the document scroll instead, and sticks the sidebar to the top of it. Better for long reading pages, and the only one where a phone's address bar still collapses on the way down.
Either way the shell hands the scrolling element to its children through context, so a TopAppBar inside it lifts and collapses against the right thing without being told which — scrollContainer becomes a thing you only reach for when the layout isn't a shell.
import { AppShell } from 'omaris' <AppShell> <AppShellSidebar> <NavigationDrawer bordered>…</NavigationDrawer> </AppShellSidebar> <AppShellMain> <TopAppBar title="Overview" /> <AppShellContent>…</AppShellContent> </AppShellMain></AppShell> Props
scroll Defaults to 'main'
NonNullable<Variants['scroll']> Which element scrolls: the content column, or the document.
class string children Snippet AppShellContent
The padded well a page's content sits in.
A dashboard reads badly at 2000px wide and badly again at 320px with no gutter, so this is the one place that owns both: a measure that caps the line length and a gutter that grows with the viewport.
import { AppShellContent } from 'omaris' Props
width Defaults to 'full'
NonNullable<Variants['width']> smmdlgxlfull
padded Defaults to true
boolean Gutter and the rhythm between sections. Off when the page brings its own.
class string children Snippet AppShellFooter
The bottom band of the content column.
sticky keeps it against the bottom of the viewport — the bar a table with a selection or a form with unsaved changes needs.
import { AppShellFooter } from 'omaris' Props
sticky Defaults to false
boolean Hold it at the bottom of the column rather than after the content.
class string children Snippet AppShellHeader
A band that stays at the top of the content column while it scrolls.
A TopAppBar can stick on its own, so this is for the cases it can't cover alone: a bar plus a filter row, a banner above the bar, anything that has to stay up there as one piece.
import { AppShellHeader } from 'omaris' Props
sticky Defaults to true
boolean Ride up with the content instead of staying put.
class string children Snippet AppShellMain
The content column of an AppShell — and, in the default scroll mode, the element that scrolls.
It registers itself with the shell, so everything inside that cares about scrolling (a TopAppBar, mostly) finds it on its own.
import { AppShellMain } from 'omaris' Props
class string children Snippet AppShellSidebar
The column an AppShell puts beside its main area — a navigation drawer, usually.
It stays put while the content scrolls, and drops out below breakpoint, where the drawer should be modal instead.
import { AppShellSidebar } from 'omaris' Props
side Defaults to 'start'
'start' | 'end' Which edge it sits on. end gives you an inspector panel instead.
breakpoint Defaults to 'md'
AppShellSidebarBreakpoint Viewport width at which the column appears. false keeps it at every size — what you want when the thing inside is already responsive.
label string Accessible name, when the column isn't a single labelled drawer.
class string children Snippet