MobileShell
The touch-app frame: a sticky app bar, one scrolling column, and a bottom nav in thumb reach.
Live demo
MobileShell is the phone counterpart to AppShell, and deliberately a separate frame rather than AppShell made responsive. AppShell is desktop chrome by construction: a three-row grid whose body is a left/main/right split with px-width resizable rails.
On a phone that model doesn't degrade so much as stop being the right shape, and bending one frame into both would make each worse. Desktop-IDE chrome and touch-app chrome are different interaction models, not one layout at two widths — so they are two components, and an app picks one at its entry.
The frame is three regions and nothing else: a sticky app bar (a heading, an optional brand, and an actions slot), a single scrollable <main> column, and a nav slot for the bottom nav, pinned within thumb reach. The shell renders that chrome itself through its fragment, exactly as AppShell does, so a mod can reshape the frame the same way it can reshape any other component.
The bar and the nav absorb the safe-area insets (env(safe-area-inset-*)), so on a notched phone the chrome takes the hardware and the content column between them is never the thing sliding underneath it. The column scrolls itself rather than the page, so the bar and the nav stay put while it moves.
When to use
How this component composes with the rest of the set.
Props
2 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Anatomy
The named parts that make up the component, with their selectors.
bar
The sticky app bar. Absorbs the top safe-area inset, so nothing sits under a notch.
title
The bar's heading, truncated rather than wrapped so a long one can't grow the bar.
actions
The trailing controls in the bar; the heading is pushed to the start, so actions collect at the end.
content
The one scrolling column, rendered as a real <main>. It scrolls itself, not the page, so the bar and the nav hold their place.
nav
The bottom nav region, usually a BottomNav. Its own bar absorbs the bottom safe-area inset.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--bg-1
--body-bg
--border-normal
--border-thick
--border-thin
--fg-0
--font-sans
--leading-normal
--line
--ring
--space-2
--space-3
--space-4
--space-6
--text-body
--text-lg
--weight-semibold
Slots
A mark rendered before the heading, e.g. a logo.
The bar's trailing controls.
The scrolling content column, rendered into <main>.
The bottom nav, usually a BottomNav.
Accessibility
Code
The frame
A sticky bar, one scrolling column, and a bottom nav. The shell renders the chrome; the slots take the app's content.
<xtyle-mobile-shell heading="Inbox">
<xtyle-button slot="actions" variant="ghost" size="sm">Filter</xtyle-button>
<!-- the one scrolling column -->
<p>Whatever the section renders.</p>
<xtyle-bottom-nav
slot="nav"
value="inbox"
label="Sections"
tabs='[{"value":"inbox","label":"Inbox","icon":"folder","badge":3},
{"value":"threads","label":"Threads","icon":"menu"},
{"value":"you","label":"You","icon":"gear"}]'
></xtyle-bottom-nav>
</xtyle-mobile-shell>
<script lang="ts">
import { MobileShell, BottomNav } from "@xtyle/svelte";
let section = $state("inbox");
const tabs = [
{ value: "inbox", label: "Inbox", icon: "folder", badge: 3 },
{ value: "threads", label: "Threads", icon: "menu" },
{ value: "you", label: "You", icon: "gear" },
];
</script>
<MobileShell heading="Inbox">
{#snippet actions()}<button>Filter</button>{/snippet}
<p>Whatever the section renders.</p>
{#snippet nav()}
<BottomNav {tabs} bind:value={section} label="Sections" />
{/snippet}
</MobileShell>
---
import MobileShell from "@xtyle/astro/MobileShell.astro";
import BottomNav from "@xtyle/astro/BottomNav.astro";
---
<MobileShell heading="Inbox">
<Fragment slot="actions">…</Fragment>
<p>Whatever the section renders.</p>
<BottomNav
slot="nav"
value="inbox"
label="Sections"
tabs={[
{ value: "inbox", label: "Inbox", icon: "folder", badge: 3 },
{ value: "threads", label: "Threads", icon: "menu" },
{ value: "you", label: "You", icon: "gear" },
]}
/>
</MobileShell>