Skip to content
omaris

Layout

App Shell

The frame a dashboard sits in: a sidebar that stays put and one column that scrolls.

import { AppShell } from 'omaris'
Learn

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. AppShellSidebar holds a NavigationDrawer, AppShellMain holds a TopAppBar and AppShellContent. Only the content column scrolls; the sidebar and bar stay put with no sticky anywhere.
  • 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 sticky holds the bottom of the column.
  • A page's reading measure. AppShellContent width defaults to full, which a table wants; md for prose, sm for 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.svelte once. Pages render inside AppShellContent.
  • Plan the phone. The sidebar column is gone below breakpoint (md by 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 AppShellHeader for a bar plus a filter row that must stay up together. A TopAppBar on its own sticks by itself and finds the scroller from the shell.

Don't

  • Put h-screen or overflow-auto on your own wrappers inside it. The shell already picked the scrolling element; a second one brings back the double scrollbar.
  • Pass scrollContainer to a TopAppBar inside the shell. It already knows.
  • Nest an AppShell per route.
  • Leave width="full" on a page of prose. Lines a screen wide do not read.

Quick reference

width
  • sm
  • md
  • lg
  • xl
  • full (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']>
sm
md
lg
xl
full
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