The app shell for an @xcwds app on @xcwds/sveltekit, from xcwds.github.io's
root layout: a header with the page title (the page's only <h1>) and a back arrow, a tab bar
on phones, header links or a sidebar on tablets and computers, one stack for toasts and
notices, a Home page other plugins add to, and an error page.
// xcwds.config.ts
import { defineConfig } from '@xcwds/core';
import shell from '@xcwds/plugin-shell';
export default defineConfig({
brand: { name: 'My app', tagline: 'Everyday tools.' },
plugins: [
shell({
sections: [
{ path: '/', label: 'Home', emoji: '๐ ' },
{ path: '/recipes', label: 'Recipes', emoji: '๐', also: ['/guide'] },
{ path: '/settings', label: 'Settings', emoji: 'โ๏ธ' }
]
})
]
});
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import Shell from '@xcwds/plugin-shell/Shell.svelte';
import UpdateBanner from '@xcwds/plugin-update/UpdateBanner.svelte';
import { App } from '@xcwds/sveltekit';
import '../app.css';
let { children } = $props();
</script>
<App>
<Shell>
{@render children()}
{#snippet notices()}<UpdateBanner />{/snippet}
</Shell>
</App>
<!-- src/routes/+page.svelte -->
<script lang="ts">
import Home from '@xcwds/plugin-shell/Home.svelte';
</script>
<Home />
<!-- src/routes/+error.svelte -->
<script lang="ts">
import ErrorPage from '@xcwds/plugin-shell/ErrorPage.svelte';
</script>
<ErrorPage />
Other pages render their content in <main class="page-narrow"> (tools, settings),
<main class="page-wide"> (lists, Home) or <main class="page-split @container"> (narrow until
xl, then wide enough for two columns, which the page lays out with @[50rem]: container
queries), and their route's width (app.route(), default wide) names the same container so
the header lines up with it. Pages don't render their own
<h1> or back links.
Styles (Tailwind v4)
The components use Tailwind classes and CSS variables; Skeleton isn't needed. Tailwind doesn't
scan node_modules, so import the shell's CSS, which adds its @source:
/* src/app.css */
@import 'tailwindcss';
@import '@xcwds/plugin-theme/tailwind.css'; /* optional: dark mode follows the theme setting */
@import '@xcwds/plugin-shell/styles.css';
It provides the sidebar: variant (the sidebar layout; use md: for anything tied to the tab
bar), the page-narrow, page-wide and page-split utilities, and the accessibility baseline: 44px controls
(checkboxes and radio buttons get theirs from their <label>), a :focus-visible ring and
reduced motion. Theme it by setting the --xcwds-shell-* and --xcwds-toast-* variables on
:root (and :root[data-color-scheme='dark']); see styles.css for the list.
Navigation
Phones (narrower than md, or under 500px tall) get the tab bar. Wider screens get header links
or a sidebar per orientation, from the nav setting ({ portrait, landscape }, each 'bar' or
'sidebar'; defaults: bar in portrait, sidebar in landscape). It applies before first paint as
data-nav-portrait / data-nav-landscape on <html>. <NavPicker> lets the user choose. With
one section there is no navigation at all.
The header's title, emoji and back arrow come from the route registry; a section's own page
uses its label, Home uses brand.name, and other pages lead back home. A section's also
paths (and everything under them) highlight it too.
For plugins
app.toast(message, { action?, durationMs? })shows a short confirmation in the stack (3 s, or 8 s with an action link{ label, path, hash? }; at most three at once). A save that fails (app.storage.onSaveFailure) shows one "Couldn't save on this device" toast.app.shell.home.add(Component, { props?, order? })adds a block to Home (lowestorderfirst), belowbrand.nameandbrand.tagline.app.shell.header.add(Component, { props?, order? })adds a button beside the title.app.shell.sectionsandapp.shell.toasts(list(),subscribe(),dismiss(id)).
Register a client plugin that uses these after the shell in the config's plugins, so
app.shell exists when it loads.
API
From the package's types and doc comments.
Options (ShellOptions)
sections?Section[]- In order. Defaults to Home alone (then there is no tab bar or sidebar).
sections.pathstring- An app path (without the base path);
/is Home. sections.labelstringsections.emoji?stringsections.also?string[]- Other paths this section owns (and everything under them), e.g.
/guidefor Recipes.
Decorators
app.shell?AppShell- From
@xcwds/plugin-shell.app.shell.sectionsreadonly Section[]app.shell.homeSlotList- Blocks on the Home page (
/), e.g. pinned and recent tools. app.shell.headerSlotList- Buttons in the header beside the title, e.g. Share.
app.shell.toasts{ list(): readonly Toast[]; subscribe(listener: (toasts: readonly Toast[]) => void): () => void; dismiss(id: number): void; }- The toasts showing now;
subscribeto follow them.
app.toast?(message: string, options?: ToastOptions) => void- From
@xcwds/plugin-shell: shows a short confirmation in the notification stack.
Settings fields
settings.navNavSetting- From
@xcwds/plugin-shell: header links or a sidebar on wider screens, per orientation.settings.nav.portraitNavStylesettings.nav.landscapeNavStyle