App Shell

The app shell wraps authenticated routes in a persistent sidebar, header, and footer layout. It manages sidebar and footer state across navigation with responsive, mobile-first behaviour built in, and lets individual pages push overrides up to the shell without coupling layout to route code.

Shell enabled

Master switch. When off, <SidebarShell> renders only its children with no header, sidebar, or footer chrome.

Visible
  • auto: rendered when any slot (left, center, right) has content, or when the sidebar would render but is mobile-collapsed (so the toggle stays reachable).
  • always: forces the header on.
  • never: hides the header everywhere.
Variant
Size
Demo content
Visible
  • auto: rendered when the current page has nav items or registered sidebar content.
  • always: forces the sidebar on.
  • never: hides the sidebar; main content fills the full width.
Variant
Size
Toggle button
Demo content
Visible
  • auto: rendered when a page mounts <ShellFooterContent> — for example, the docs prev/next nav on this page.
  • always: forces the footer on (renders even when empty).
  • never: hides the footer.
Variant
Size
Demo content
Visible
  • auto: slides in when the aside has slot content (any of head, body, or foot is non-null and not disabled) and shell.aside.open is true. The Mini-chat trigger toggle in the Header tab pushes the trigger button into the topbar; clicking it flips shell.aside.open.
  • always: forces the aside on screen — overrides open, so the trigger has no visual effect.
  • never: hides the aside (the default at config level).
Variant
Layout
Demo content

Features

  • Element visibility — sidebar, header, footer, and aside each take a visible: 'auto' | 'always' | 'never' setting. auto (default for sidebar/header/footer; the aside defaults to 'never') renders the element when it has content (sidebar / aside: any of head, body, or foot is non-null and not disabled; footer: footer content snippet; header: any of the three slot regions has content, or the sidebar would render but is mobile-collapsed — note: aside presence does NOT trigger header auto-show). always forces it on; never forces it off.
  • Master switch — top-level enabled: boolean (default true). When false, <SidebarShell> renders only its children with no chrome — useful for full-screen or embedded views.
  • Sidebar slots — three optional regions: head (logo / branding via <ShellSidebarLogo>), body (nav prop or a content snippet override), foot (profile menu via <ShellSidebarProfile>). Mount each component from a layout — they're optional, no built-in defaults.
  • Sidebar variants — flat (sidebar flush with content edge, header starts after it), transparent (same but no sidebar background fill), or floating (sidebar card floats above content, header spans full width).
  • Minimized state — sidebar collapses to an icon strip; persisted in localStorage and restored on next load.
  • Auto-minimize — optionally collapses the sidebar when the viewport is narrower than a configurable threshold (desktop only — below 767 px the mobile drawer takes over).
  • Mobile drawer — on small screens the sidebar slides off-screen; the toggle opens it as an overlay with a dimmed backdrop.
  • Header — fixed top bar with a three-area slot layout (left, center, right). The left area always includes the sidebar toggle; additional content is injected via <ShellHeaderContent>. Supports ordering variants (ltr, centered, rtl) and visual variants (bordered, flat, elevated), controllable per-page via useShellContext().
  • Footer — a fixed bar at the bottom of the content area. Appears only when a page mounts <ShellFooterContent>. Three visual variants: flat (solid bar), transparent (gradient fade, content scrolls underneath), floating (elevated card above the page edge).
  • Aside (right sidebar) — opt-in panel anchored to the right edge. Mirrors the primary sidebar's slot/visibility/variant surface (head/body/foot via <AsideContent>, auto/always/never, flat/transparent/floating) but uses a binary open state instead of minimize/peek: it's off-canvas by default and slides in when shell.aside.open flips. A layout: 'overlap' | 'squash' axis decides how the aside affects the rest of the shell — overlap (default) slides over content/header/footer, squash reserves space so they shrink. Disabled at config level by default; the demo turns it on to host the mini-chat. Aside presence is purely additive — it does not force the header to appear on mobile when the sidebar is collapsed.
  • Footer fit — container (footer inner spans the content column width) or content (footer inner shrinks to its content, centered).
  • Content column alignment — the footer inner is constrained to contentMaxWidth and uses the same horizontal padding as .page, so nav buttons align flush with body text at every viewport width.
  • Per-page overrides — any child page can call useShellContext() to override sidebar variant, minimized state, auto-minimize threshold, footer variant, footer fit, content max-width, header ordering, header variant, or provide custom sidebar/footer/header content — without touching the layout file.

Setup

Local dev

No environment variables required. Configuration lives in src/lib/configs/shell.config.ts:

import { defaultShellConfig, type ShellConfig } from '$lib/svelm/shell/config';

export const shellConfig: ShellConfig = {
	...defaultShellConfig,
	enabled: true, // master switch — set false to bypass the entire shell
	sidebar: {
		...defaultShellConfig.sidebar,
		visible: 'auto', // 'auto' | 'always' | 'never'
		variant: 'floating', // 'flat' | 'transparent' | 'floating'
		expanded: true, // used only before localStorage is written
		autoMinimizeBelow: 1024 // px — omit to disable
	},
	header: {
		...defaultShellConfig.header,
		visible: 'auto' // 'auto' | 'always' | 'never'
	},
	footer: {
		...defaultShellConfig.footer,
		visible: 'auto', // 'auto' | 'always' | 'never'
		variant: 'transparent' // 'flat' | 'transparent' | 'floating'
	},
	content: {
		maxWidth: '52rem' // aligns footer with page body column
	},
	aside: {
		...defaultShellConfig.aside,
		visible: 'never', // 'auto' | 'always' | 'never' — disabled at template default
		variant: 'flat', // 'flat' | 'transparent' | 'floating'
		layout: 'overlap', // 'overlap' | 'squash' — slides over content vs. shrinks it
		open: false // off-canvas; flip via shell.aside.open at runtime
	}
};

Staging / prod

No secrets or runtime variables required.

Implementation details

The shell library splits into a shell/ container and a shell/content/ collection of optional, pure UI pieces. Container components define structure and slot contracts; content components don't know about slots. The user wraps content in a slot wrapper to position it.

Container — `src/lib/svelm/shell/`

  • <Shell> — shell.svelte. Top-level container. Owns sidebar state (minimized, hidden, peeking), creates the shell context, resolves visibility / master switch, renders header / sidebar / main / footer regions. Props: user, pageTitle, config, children.
  • <ShellHeaderContent location='left|center|right'> — header-content.svelte. Slot wrapper; the children snippet is contributed to the corresponding header region.
  • <ShellSidebarContent location='head|body|foot'> — sidebar-content.svelte. Slot wrapper; the children snippet is contributed to the corresponding sidebar region. Single contributor per location, last-write-wins.
  • <ShellFooterContent> — footer-content.svelte. Slot wrapper for the bottom bar. Single slot, no location.
  • <AsideContent location='head|body|foot'> — aside-content.svelte. Slot wrapper for the right-side aside. Same contract as <ShellSidebarContent> (single contributor per location, last-write-wins). When at least one slot is populated, the aside's auto visibility predicate flips on.
  • Shell context — shell-context.svelte.ts — createShellContext() in the shell root, getShellContext() / useShellContext() / tryGetShellContext() in children. useShellContext(getter) runs the getter synchronously (SSR-safe) and inside a reactive $effect, then nulls owned fields on teardown.
  • Internal layout — header.svelte, sidebar.svelte, aside.svelte — pure layout shells consumed by <Shell>. They render the slot snippets stored on the context; no nav, logo, or profile rendering lives here. The aside is right-anchored: when shell.aside.open is false it's translated off-canvas; when true it slides in at z-index --z-aside (defaults to --z-sidebar). In layout: 'squash' mode the fixed header/footer right edge retreats by --aw and a right-side flex gap shrinks the main column; in layout: 'overlap' (default) nothing else moves and the aside covers content/bars on its way in.
  • <SidebarToggleButton> — sidebar-toggle.svelte — backstop button used inside the sidebar head and as a standalone fixed control when the page header is off and the sidebar is mobile-collapsed.
  • <SidebarEscShortcut> — sidebar-esc-shortcut.svelte — closes the mobile drawer on Escape.
  • Config — config.ts (ShellConfig, defaultShellConfig); types.ts (NavSection, NavItem); nav-direction.ts (view-transition direction signal).

Content — `src/lib/svelm/shell/content/`

Each one renders a self-contained piece of UI. None knows where it goes — wrap it in a slot.

  • <ShellLogo mark label href?> — logo.svelte. Brand mark + wordmark.
  • <ShellNav nav={NavSection[]} /> — nav.svelte. Renders sections, items, active-route detection, collapsed-mode tooltips. Writes shell.sidebar.resolvedNav so downstream consumers (e.g. <ShellFooterNav>) can compute prev/next without re-importing the nav data source.
  • <ShellProfile /> — profile.svelte. Reads shell.user from context, renders <ProfileMenu>.
  • <ShellPageTitle /> — page-title.svelte. Reads shell.pageTitle from context.
  • <ShellBreadcrumb root={...} /> — breadcrumb.svelte. Derives a breadcrumb from the current pathname, optionally rooted in a path prefix.
  • <ShellFooterNav showNav showPageTitle /> — footer-nav.svelte. Prev/next links from shell.sidebar.resolvedNav plus an optional centered page title. Owns the ArrowLeft/ArrowRight keybindings.

For a theme toggle in the header, drop the project's existing <ThemeToggle> (from $lib/components/ui/theme-toggle) directly into a <ShellHeaderContent location="right"> — no shell wrapper needed.

Visibility & master switch

  • Each region resolves Visibility (auto | always | never) inside shell.svelte. auto is gated by a per-region "has content" predicate (sidebar: any of head, body, or foot; footer: footer content; header: any populated header region OR sidebar-mobile-collapsed; aside: any of head, body, or foot, ignoring sidebar/header state). Override per-page via useShellContext({ sidebar: { visible: 'never' } }) etc.
  • Master switch — shell.enabled (or shellConfig.enabled for the static default) bypasses the entire shell. When false, <Shell> renders only its children, the document is not locked (shell-active class is dropped), and shell.{header,sidebar,aside}.{isVisible,isPresent,isOpen} all report false.

Aside open state

The aside is the only shell element with an open flag. shell.aside.open (or shellConfig.aside.open for the static default) toggles between off-canvas (closed) and slid-in (open) — but only when visible === 'auto'. visible: 'always' forces the aside on screen and visible: 'never' forces it off, regardless of open; the flag is consulted only in 'auto' mode (and additionally requires actual slot content to slide in). Hosts wire open however they like — for example, the demo layout pushes the mini-chat context's open flag through:

const miniChat = getMiniChatContext();
useShellContext(() => ({
	aside: { visible: 'auto', variant: 'flat', open: miniChat.open }
}));

The mini-chat surface itself splits into two decoupled components:

  • <MiniChatTrigger /> — pure UI button (? icon). Reads/writes miniChat.open. Knows nothing about its host. Drop into a <ShellHeaderContent location="right">, a sidebar foot, or anywhere else a help affordance fits.
  • <MiniChat /> — inline chat panel. Always renders when mounted; the host decides where it appears (e.g. inside an <AsideContent location="body">).

Z-index stack

CSS variables on .shell-layout: --z-footer: 10, --z-backdrop: 11, --z-header: 12, --z-sidebar: 13, --z-aside: 13. Adjust in one place to reorder the whole stack. The aside sits at the same level as the primary sidebar so a slid-in panel cleanly covers the header bar to its right.

AboutDocDemo