Layout
AppShell
Application layout with a module rail, a navigation panel, a mobile header and a bottom navigation, independent of any router.
<script>
import { AppShellLayout, type NavModule } from "@codeatlasdev/ui";
import { goto } from "$app/navigation"; // or any router
import { page } from "$app/state";
const modules: NavModule[] = [/* id, label, icon, basePath, items */];
</script>
<AppShellLayout {modules} pathname={page.url.pathname} onNavigate={(path) => goto(path)}>
{#snippet header()}<SiteHeader />{/snippet}
{@render children()}
</AppShellLayout>
The shell never imports a router. You pass the current pathname and handle onNavigate, so it works with SvelteKit, Astro, or plain state. onNavigate fires when the user switches module from the rail or the bottom navigation. Panel items are plain <a href> links, so a client-side router takes them over like any other link. Put it only on application routes, never on marketing pages. Below 900px the rail and panel disappear and the mobile header plus bottom navigation take over. See the templates for complete applications built on it.
Anatomy
Rail (52px), panel (200px) and the main content area.
52px
200px
Code
Live shell
The real component inside a fixed-height frame. The pathname lives in local state; click the rail and panel to navigate.
Revenue
Users
Click a rail icon or a panel item. The demo keeps the pathname in local state.
Code
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modules | NavModule[] | – | Navigation modules. Each has id, label, icon, basePath, optional pathPrefixes and items (NavItem or NavSection). |
pathname | string | – | Current location. Decides the active module and item. |
onNavigate | (path: string) => void | – | Called with the destination path when the rail, panel or bottom navigation is used. |
defaultModule | number | 0 | Module index used before any navigation matches. |
persistPanel | boolean | true | Keeps the shell across page loads: panel open or closed, selected module and expanded sections (localStorage), panel scroll (sessionStorage). Restored before the first paint. Turn it off for demos and embeds that must always start from the defaults. |
header | Snippet | – | Desktop header rendered above the page content. Hidden below 900px. |
brand | Snippet | – | Logo shown at the top of the rail and in the mobile header. |
railBottom | Snippet | – | Items pinned to the bottom of the rail, such as an avatar or notifications. |
mobileHeaderActions | Snippet | – | Controls at the trailing edge of the mobile header. Use it for anything that must stay reachable below 900px. |
mobileTitle | Snippet | – | Replaces the plain page title in the mobile header, for example a tappable title that opens a sheet. |
mainId | string | null | 'main' | `id` of the content region, the skip link target. Pass `null` when your page renders its own `<main id="main" tabindex="-1">`. |
skipLink | boolean | true | Renders the SkipLink. Turn it off when the shell is embedded in a page that already has one. |
children | Snippet | – | Page content. |
State across page loads
On a multi-page site (Astro, a full reload) every link loads a new document, so a shell that only keeps state in memory reopens at its defaults: the panel, the module, the expanded groups and the panel scroll are all lost. With persistPanel (the default) the shell keeps them and puts them back before the first paint, the same way the theme script does.
| What | Where | Key |
|---|---|---|
| Panel open or closed, selected module, section open map | localStorage |
ca-shell |
| Panel scroll, per module | sessionStorage |
ca-shell-scroll |
An inline script emitted right after the sidebar reads those keys and sets data-ca-panel="open|closed" and data-ca-module="<id>" on <html> before anything paints, so a panel the user closed is laid out closed from the first frame instead of flashing open. Stored values are validated and discarded when malformed. A section’s last open or closed choice wins over defaultOpen, but the section that holds the current page always opens. The module comes from the URL when modules matches it, and from the stored choice otherwise.
Render the shell on the server so the first paint is not blank: the inline script and the markup travel together. A client-only mount gets the same restore on hydration, and a page whose CSP blocks inline scripts too. Keep id stable on each NavModule (it is the stored module key) and each section label unique within a module (it is the stored section key).