PortalShell
The application frame: sidebar, toolbar, and page content.
The shell is the application frame. Open it on its own so the sidebar, toolbar, and mobile navigation can use the viewport.
Open the shellSource
<script lang="ts">
import 'portal-bits/styles.css';
import 'portal-bits/tokens.css';
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
{@render children()}
<script lang="ts">
import PortalShellExample from './PortalShellExample.svelte';
</script>
<PortalShellExample />
<script lang="ts">
import { Button, PortalShell, TopBar, type NavItem } from 'portal-bits';
// This example uses @lucide/svelte for the navigation icons. Omit `icon` to skip it.
import { Component, Layers, LayoutGrid } from '@lucide/svelte';
const items: NavItem[] = [
{ href: '#overview', label: 'Overview', icon: LayoutGrid },
{ href: '#foundations', label: 'Foundations', icon: Layers },
{ href: '#components', label: 'Components', icon: Component, count: '29' },
];
// The consumer supplies the active URL. In SvelteKit, use the current pathname.
let active = $state('#overview');
let collapsed = $state(false);
</script>
<PortalShell {items} {active} brandHref="#overview" brandLabel="Example Studio" bind:collapsed>
{#snippet brand()}<span>Example Studio</span>{/snippet}
{#snippet summary()}
<p>A familiar feel.<br />An open foundation.</p>
{/snippet}
{#snippet footer()}
<Button variant="quiet" onclick={() => (collapsed = !collapsed)}>
{collapsed ? 'Expand' : 'Collapse'} sidebar
</Button>
{/snippet}
{#snippet topbar()}
<TopBar breadcrumb="Discover" />
{/snippet}
<section>
<h1>Overview</h1>
<p>Your page content goes here.</p>
</section>
</PortalShell>
API reference
These tables list the props and snippets that PortalShell declares. Attributes passed through to an element or primitive are listed under Forwarded attributes, not here.
Props
| Name | Type | Default | Required | Binding | Description |
|---|---|---|---|---|---|
items | NavItem[] | None | Yes | One-way | Navigation items as `{ href, label, icon?, count? }`. The sidebar and mobile bar share them. |
active | string | None | Yes | One-way | The `href` of the current item. Compared with exact equality. |
collapsed | boolean | false | No | Bindable | Whether the sidebar shows icons only. |
brandHref | string | / | No | One-way | The destination of the brand link. |
brandLabel | string | None | No | One-way | The brand link name. Without it, the visible brand names the link, or "Home" when collapsed. |
Snippets
| Name | Type | Default | Required | Binding | Description |
|---|---|---|---|---|---|
brand | Snippet | None | No | One-way | Replaces the default "portalbits" wordmark. |
summary | Snippet | None | No | One-way | Content under the brand. Hidden when collapsed. |
footer | Snippet | None | No | One-way | Content at the foot of the sidebar. Hidden when collapsed. |
topbar | Snippet | None | No | One-way | The toolbar above the page content. |
children | Snippet | None | No | One-way | The page content, inside `main`. |
Forwarded attributes
None. PortalShell forwards no attributes.
No data attributes are documented as stable.
Limits
- The consumer supplies `active`. The shell reads no URL.
- The skip link targets a fixed `main` with the id `main-content`.
- The mobile navigation appears at narrow widths. That breakpoint is an adaptation.
- The shell needs the full viewport. The gallery links to it instead of embedding it.