MobileShell
The mobile app frame. MobileShell is a fixed, full-height column: a pinned PageHeaderTarget on top (with safe-area padding for installed PWAs), a native-scrolling content area, and a #nav slot for a bottom MobileNav. It's a separate family from DesktopShell — mobile and desktop are different navigation models, so the app chooses which to render for the viewport rather than toggling one responsive component.
<script setup lang="ts">
import { ref } from 'vue'
import {
MobileShell,
MobileNav,
MobileNavItem,
PageHeaderMobile,
Avatar,
} from 'frappe-ui'
const tab = ref('home')
</script>
<template>
<!--
MobileShell is `fixed inset-0`. The `transform` on this wrapper makes it the
containing block for that fixed positioning, so the shell stays inside the
preview box instead of covering the page. (A story-only concern.)
-->
<div
class="mx-auto h-[600px] w-[360px] transform-gpu overflow-hidden rounded-xl border bg-surface-white"
>
<MobileShell>
<PageHeaderMobile>
<span class="text-base font-semibold text-ink-gray-8">Home</span>
</PageHeaderMobile>
<div class="p-4">
<p
v-for="n in 30"
:key="n"
class="mb-3 text-base text-ink-gray-7"
>
Row {{ n }} — scroll; the header and tab bar stay fixed.
</p>
</div>
<template #nav>
<MobileNav>
<MobileNavItem
label="Home"
icon="lucide-house"
:active="tab === 'home'"
@click="tab = 'home'"
/>
<MobileNavItem
label="Notifications"
icon="lucide-bell"
:active="tab === 'notifications'"
@click="tab = 'notifications'"
/>
<MobileNavItem
label="Search"
icon="lucide-search"
:active="tab === 'search'"
@click="tab = 'search'"
/>
<MobileNavItem
label="You"
:active="tab === 'you'"
@click="tab = 'you'"
>
<template #default="{ active }">
<Avatar
label="Jane Doe"
size="md"
:class="active ? 'ring-2 ring-outline-gray-4' : ''"
/>
</template>
</MobileNavItem>
</MobileNav>
</template>
</MobileShell>
</div>
</template>Put the routed page in the default slot and the tab bar in #nav. The content area scrolls with the platform's own momentum and overscroll; the header and nav stay fixed to the edges.
Scroll container
Like DesktopShell, the content area registers into the scroll-container registry, so useScrollContainer() and getScrollContainer() resolve it — a tapped-active tab or a router scrollBehavior can drive it with no app-owned global.
API Reference
| Slot | Payload |
|---|---|
default | {} |
nav | {} |