Frappe UIFrappe UI

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.

Home

Row 1 — scroll; the header and tab bar stay fixed.

Row 2 — scroll; the header and tab bar stay fixed.

Row 3 — scroll; the header and tab bar stay fixed.

Row 4 — scroll; the header and tab bar stay fixed.

Row 5 — scroll; the header and tab bar stay fixed.

Row 6 — scroll; the header and tab bar stay fixed.

Row 7 — scroll; the header and tab bar stay fixed.

Row 8 — scroll; the header and tab bar stay fixed.

Row 9 — scroll; the header and tab bar stay fixed.

Row 10 — scroll; the header and tab bar stay fixed.

Row 11 — scroll; the header and tab bar stay fixed.

Row 12 — scroll; the header and tab bar stay fixed.

Row 13 — scroll; the header and tab bar stay fixed.

Row 14 — scroll; the header and tab bar stay fixed.

Row 15 — scroll; the header and tab bar stay fixed.

Row 16 — scroll; the header and tab bar stay fixed.

Row 17 — scroll; the header and tab bar stay fixed.

Row 18 — scroll; the header and tab bar stay fixed.

Row 19 — scroll; the header and tab bar stay fixed.

Row 20 — scroll; the header and tab bar stay fixed.

Row 21 — scroll; the header and tab bar stay fixed.

Row 22 — scroll; the header and tab bar stay fixed.

Row 23 — scroll; the header and tab bar stay fixed.

Row 24 — scroll; the header and tab bar stay fixed.

Row 25 — scroll; the header and tab bar stay fixed.

Row 26 — scroll; the header and tab bar stay fixed.

Row 27 — scroll; the header and tab bar stay fixed.

Row 28 — scroll; the header and tab bar stay fixed.

Row 29 — scroll; the header and tab bar stay fixed.

Row 30 — scroll; the header and tab bar stay fixed.

vue
<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

default
{}
nav
{}