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.
Master switch. When off, <SidebarShell> renders only its children with no header,
sidebar, or footer chrome.
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.
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.
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.
auto: slides in when the aside has slot content (any of head, body, or foot is non-null and not disabled) andshell.aside.openis true. The Mini-chat trigger toggle in the Header tab pushes the trigger button into the topbar; clicking it flipsshell.aside.open.always: forces the aside on screen — overridesopen, so the trigger has no visual effect.never: hides the aside (the default at config level).
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).alwaysforces it on;neverforces it off. - Master switch — top-level
enabled: boolean(defaulttrue). Whenfalse,<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 acontentsnippet 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), orfloating(sidebar card floats above content, header spans full width). - Minimized state — sidebar collapses to an icon strip; persisted in
localStorageand 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 viauseShellContext(). - 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 binaryopenstate instead of minimize/peek: it's off-canvas by default and slides in whenshell.aside.openflips. Alayout: 'overlap' | 'squash'axis decides how the aside affects the rest of the shell —overlap(default) slides over content/header/footer,squashreserves 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) orcontent(footer inner shrinks to its content, centered). - Content column alignment — the footer inner is constrained to
contentMaxWidthand 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'sautovisibility 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: whenshell.aside.openis false it's translated off-canvas; when true it slides in at z-index--z-aside(defaults to--z-sidebar). Inlayout: 'squash'mode the fixed header/footer right edge retreats by--awand a right-side flex gap shrinks the main column; inlayout: '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. Writesshell.sidebar.resolvedNavso downstream consumers (e.g.<ShellFooterNav>) can compute prev/next without re-importing the nav data source.<ShellProfile />—profile.svelte. Readsshell.userfrom context, renders<ProfileMenu>.<ShellPageTitle />—page-title.svelte. Readsshell.pageTitlefrom context.<ShellBreadcrumb root={...} />—breadcrumb.svelte. Derives a breadcrumb from the current pathname, optionally rooted in apathprefix.<ShellFooterNav showNav showPageTitle />—footer-nav.svelte. Prev/next links fromshell.sidebar.resolvedNavplus 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) insideshell.svelte.autois 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 viauseShellContext({ sidebar: { visible: 'never' } })etc. - Master switch —
shell.enabled(orshellConfig.enabledfor the static default) bypasses the entire shell. When false,<Shell>renders only itschildren, the document is not locked (shell-activeclass is dropped), andshell.{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/writesminiChat.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.