PWA UIv0.1 beta
Docs / Guides / App layout

Guide

App layout

Build a full-screen application frame with persistent navigation, one predictable scroll region, and safe-area behavior that is applied exactly once.

Mental model

Each layer owns one concern. The provider measures the viewport, the application root contains document scrolling, AppShell divides the viewport, and the navigation components render inside its placement regions.

Composition
PWAProvider└── Application root containment    └── AppShell        ├── AppShell.Header        │   └── NavigationBar        ├── AppShell.Main        │   └── Routed screen content        └── AppShell.Footer            └── TabBar

Installation

Install the shared provider and the layout components together. The registry dependencies also install the PWA UI base stylesheet; import that stylesheet once as shown below.

Terminal
pnpm dlx shadcn@latest add \  @pwa-ui/pwa-provider \  @pwa-ui/app-shell \  @pwa-ui/navigation-bar \  @pwa-ui/tab-bar \  @pwa-ui/safe-area

Responsibilities

LayerOwnsDoes not own
PWAProviderViewport and keyboard measurementsDocument scrolling or visual chrome
Application rootContaining document scroll for full-screen appsScreen regions or navigation
AppShellOne visual-viewport frame and region placementNavigation appearance or routing
AppShell.MainThe application’s vertical scrollingPersistent header or footer chrome
NavigationBarTop navigation content and actionsSafe-area placement or viewport height
TabBarPrimary destinations and active stateFooter placement or document scrolling
SafeAreaInsets explicitly requested by custom layoutsInsets AppShell already applies

Contain the application root

A full-screen application should not let the browser document become a second scroll container. Import the PWA UI base stylesheet, add data-pwa-app-root to the document, and mark the element that directly contains your application.

Opt in only for application surfaces. Leave these attributes off documentation, marketing, and embedded pages that should use normal document scrolling.

Next.js

app/layout.tsx
import { PWAProvider } from "@/components/ui/pwa-provider"import "@/styles/pwa.css"
export default function RootLayout({ children }) {  return (    <html lang="en" data-pwa-app-root>      <body>        <PWAProvider>          <div data-pwa-app-mount>{children}</div>        </PWAProvider>      </body>    </html>  )}

Vite or React

index.html
<!-- index.html --><html lang="en" data-pwa-app-root>  <body>    <div id="root" data-pwa-app-mount></div>    <script type="module" src="/src/main.tsx"></script>  </body></html>

Enable safe-area insets

Use viewport-fit=cover so installed and edge-to-edge browser surfaces expose their safe-area environment variables. In Next.js, export the viewport configuration from your root layout.

app/layout.tsx
import type { Viewport } from "next"
export const viewport: Viewport = {  width: "device-width",  initialScale: 1,  viewportFit: "cover",}

For a static HTML entry, add viewport-fit=cover to the viewport meta tag instead.

Compose the shell

Place visual navigation inside the shell regions. Keep routed screen content inside Main so navigation remains anchored while content changes and scrolls.

application-layout.tsx
import Link from "next/link"import { Home, Search, User } from "lucide-react"
import { AppShell } from "@/components/ui/app-shell"import { NavigationBar } from "@/components/ui/navigation-bar"import { TabBar } from "@/components/ui/tab-bar"
export function ApplicationLayout({ children }) {  return (    <AppShell>      <AppShell.Header>        <NavigationBar>          <NavigationBar.Title>Field Notes</NavigationBar.Title>        </NavigationBar>      </AppShell.Header>
      <AppShell.Main>{children}</AppShell.Main>
      <AppShell.Footer keyboardBehavior="hide">        <TabBar>          <TabBar.Item render={<Link href="/" />} icon={<Home />} label="Home" active />          <TabBar.Item render={<Link href="/search" />} icon={<Search />} label="Search" />          <TabBar.Item render={<Link href="/profile" />} icon={<User />} label="Profile" />        </TabBar>      </AppShell.Footer>    </AppShell>  )}

Safe-area ownership

  • AppShell.Header applies the top inset. Do not add another top SafeArea around NavigationBar.
  • AppShell.Footer applies the bottom inset. Do not add another bottom SafeArea around TabBar.
  • Use SafeArea for standalone custom chrome or for explicit left and right protection in landscape layouts.
  • Keep ordinary content spacing inside a child element when it must be additive to a safe-area inset.

Routing and persistence

Mount PWAProvider and the application shell in a persistent router layout, then render the route outlet inside AppShell.Main. Pass the active route to TabBar and screen-specific titles or actions to NavigationBar without remounting the frame.

Common mistakes

  • Allowing both the document and AppShell.Main to scroll.
  • Placing NavigationBar or TabBar beside AppShell instead of inside its Header or Footer.
  • Applying the same safe-area edge in both AppShell and SafeArea.
  • Mounting a separate AppShell for every route when the navigation chrome should persist.
  • Using 100vh wrappers outside the shell that can exceed the usable mobile visual viewport.