Skip to content
Preset · built on dashboard

Mobile kit

Start the app kit as an installable PWA and a Capacitor shell with native lifecycle hooks.

For: Teams who want one app on the web, installed from the browser, and in the App Store and Google Play.

kitr new my-app --kit mobile

First time? Run npm i -g @kitr/kitr once. Inside a project, the global kitr runs your project's own version.

kitr dev site
The app in a browser, as a PWA
kitr dev phone --target ios
The Capacitor app in the iOS simulator
kitr build phone
The native projects for Xcode and Android Studio

An installable web app

The site is a PWA with an install button, an online and offline status, and a prompt when an update is ready.

A Capacitor app

platforms/phone/ wraps the same build for iOS and Android.

Native lifecycle

Inside Capacitor the home page follows the app going to the background and the hardware back button.

The dashboard underneath

Sign-in, forms, tables and the eleven components.

What's inside

Read from the kit's output in the kitr repository, so it matches what kitr new writes today. 16 packages · 12 components · 52 files.

Platforms
site: webspaphone: mobile
Packages
routerplatformstoragequeryhttpformstablestatetailwindthemetoastauthmockstestingdevtoolsvite
Components
app-shellbadgebuttoncarddata-tableempty-stateformiconinputlabelpage-headertheme-toggle

Every file it writes

Start with app/pages/home.tsx. Pick any file to read it.

Source
app/pages/home.tsx
import { useAppState, useBackButton, useOnline, usePlatform } from '@kitr/platform/react'
import { useInstallPrompt } from '@kitr/web/client'
import { useCallback, useState } from 'react'
import { Badge } from '#components/ui/badge'
import { Button } from '#components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '#components/ui/card'
import { PageHeader } from '#components/ui/page-header'

/**
 * One page, three homes: the browser, the installed PWA, and the Capacitor shell on iOS/Android.
 * Everything below works in all three; `@kitr/platform` answers the same way everywhere.
 */
export default function HomePage() {
  const platform = usePlatform()
  const online = useOnline()
  const install = useInstallPrompt()
  const active = useAppState()
  const [backPresses, setBackPresses] = useState(0)
  useBackButton(useCallback(() => setBackPresses((n) => n + 1), []))

  return (
    <main className="mx-auto max-w-lg p-4 pb-[env(safe-area-inset-bottom)]">
      <PageHeader
        title="Mobile"
        description="Platform-aware without user-agent sniffing."
        actions={<Badge variant="secondary">{platform.type} · {platform.os}</Badge>}
      />
      <div className="grid gap-4">
        <Card>
          <CardHeader>
            <CardTitle>Connectivity</CardTitle>
            <CardDescription>`useOnline()` tracks the online and offline events.</CardDescription>
          </CardHeader>
          <CardContent>
            <Badge variant={online ? 'default' : 'destructive'}>{online ? 'Online' : 'Offline'}</Badge>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>Install</CardTitle>
            <CardDescription>
              The manifest and service worker come from the web platform's pwa option in kitrrc.ts; `useInstallPrompt()` wraps the browser prompt.
            </CardDescription>
          </CardHeader>
          <CardContent className="flex items-center gap-3">
            <Button disabled={!install.canInstall} onClick={() => install.install()}>
              Add to home screen
            </Button>
            <span className="text-sm text-muted-foreground">
              {install.installed
                ? 'Installed.'
                : install.canInstall
                  ? 'Ready to install.'
                  : platform.native
                    ? 'Running inside the native shell.'
                    : 'Open in a browser that supports installation.'}
            </span>
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>Native lifecycle</CardTitle>
            <CardDescription>Only wired up inside Capacitor; harmless on the web.</CardDescription>
          </CardHeader>
          <CardContent className="grid gap-1 text-sm text-muted-foreground">
            <p>App state: {active ? 'active' : 'background'}</p>
            <p>Hardware back button presses: {backPresses}</p>
          </CardContent>
        </Card>
      </div>
    </main>
  )
}

Other kits

All kits →