Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions capstart-website/content/docs/transitions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Transitions
description: Add framework-agnostic page transitions to a Capacitor app.
---

`@capgo/transitions` adds native-feeling page transitions to a Capacitor app without forcing a design system or a router.
`@capgo/capacitor-transitions` adds native-feeling page transitions to a Capacitor app without forcing a design system or a router.

It is a separate library from `@capgo/capacitor-native-navigation`. You can use both together: Native Navigation can render the native navbar and tabbar, while Transitions animates the page content underneath.

Expand Down Expand Up @@ -32,11 +32,23 @@ It is a separate library from `@capgo/capacitor-native-navigation`. You can use

## Installation

### Vite + Capacitor + shadcn/ui

If you use Vite, Capacitor, React, and shadcn/ui, install the complete React navigation shell from the Capstart registry:

```bash
npx shadcn@latest add AdrienADV/capstart/react-capacitor-navigation
```

The registry installs the transition setup, a React Router shell, a bottom tab bar, a native-feeling header, example screens, `viewport-fit=cover`, and the minimal safe-area CSS variables needed by the layout.

### Manual installation

```bash
npm install @capgo/transitions
npm install @capgo/capacitor-transitions
```

The package is published as `@capgo/transitions`. The GitHub repository is named `capacitor-transitions`.
The package is published as `@capgo/capacitor-transitions`. The GitHub repository is named `capacitor-transitions`.

## Basic Structure

Expand Down Expand Up @@ -69,8 +81,8 @@ import { useEffect, useRef } from 'react';
import {
initTransitions,
setupRouterOutlet,
} from '@capgo/transitions/react';
import '@capgo/transitions';
} from '@capgo/capacitor-transitions/react';
import '@capgo/capacitor-transitions';

initTransitions({ platform: 'auto' });

Expand All @@ -92,7 +104,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
```tsx title="HomePage.tsx"
import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { setDirection, setupPage } from '@capgo/transitions/react';
import { setDirection, setupPage } from '@capgo/capacitor-transitions/react';

export function HomePage() {
const navigate = useNavigate();
Expand Down Expand Up @@ -159,7 +171,7 @@ export function HomePage() {
For lower-level control, create a transition controller and drive the stack manually.

```ts title="transition-controller.ts"
import { createTransitionController } from '@capgo/transitions';
import { createTransitionController } from '@capgo/capacitor-transitions';

const controller = createTransitionController({
platform: 'auto',
Expand All @@ -182,11 +194,11 @@ await controller.setRoot(homePageElement, {

## Use With Native Navigation

`@capgo/transitions` and `@capgo/capacitor-native-navigation` solve different parts of the interface.
`@capgo/capacitor-transitions` and `@capgo/capacitor-native-navigation` solve different parts of the interface.

| Library | Responsibility |
| --- | --- |
| `@capgo/transitions` | Animates web pages, headers, content, and footers inside the WebView. |
| `@capgo/capacitor-transitions` | Animates web pages, headers, content, and footers inside the WebView. |
| `@capgo/capacitor-native-navigation` | Renders native navbar and tabbar surfaces around the WebView. |

When you use both together, keep the native bars outside the animated page content. Let Native Navigation own the bars, and let Transitions animate the route content.
Expand Down
92 changes: 92 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "capstart",
"homepage": "https://github.com/AdrienADV/capstart",
"items": [
{
"name": "react-capacitor-navigation",
"type": "registry:item",
"title": "React Capacitor Navigation",
"description": "Mobile-first React Router shell for Capacitor apps with Capgo transitions, a tab layout, a native-feeling header, safe-area variables, and example screens.",
"dependencies": [
"@capacitor/core",
"@capgo/capacitor-transitions",
"lucide-react",
"react-router"
],
"registryDependencies": [
"button"
],
"css": {
":root": {
"--safe-area-top": "var(--safe-area-inset-top, env(safe-area-inset-top, 0px))",
"--safe-area-bottom": "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))"
},
"html, body, #root": {
"height": "100%"
},
"body": {
"margin": "0",
"overflow": "hidden"
},
"cap-router-outlet": {
"display": "block",
"height": "100%"
}
},
"files": [
{
"path": "registry/react-capacitor-navigation/index.html",
"type": "registry:file",
"target": "~/index.html"
},
{
"path": "registry/react-capacitor-navigation/src/main.tsx",
"type": "registry:file",
"target": "~/src/main.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/app.tsx",
"type": "registry:file",
"target": "~/src/app.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/router.tsx",
"type": "registry:file",
"target": "~/src/router.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/components/capacitor-header.tsx",
"type": "registry:file",
"target": "~/src/components/capacitor-header.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/hooks/use-capacitor-page.ts",
"type": "registry:file",
"target": "~/src/hooks/use-capacitor-page.ts"
},
{
"path": "registry/react-capacitor-navigation/src/components/capacitor-tab-bar.tsx",
"type": "registry:file",
"target": "~/src/components/capacitor-tab-bar.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/pages/app/home.tsx",
"type": "registry:file",
"target": "~/src/pages/app/home.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/pages/app/settings.tsx",
"type": "registry:file",
"target": "~/src/pages/app/settings.tsx"
},
{
"path": "registry/react-capacitor-navigation/src/pages/app/home/details.tsx",
"type": "registry:file",
"target": "~/src/pages/app/home/details.tsx"
}
],
"docs": "This item installs a starter navigation shell and targets index.html, src/main.tsx, src/app.tsx and src/router.tsx. Run `shadcn add AdrienADV/capstart/react-capacitor-navigation --dry-run` first in existing projects. For Capacitor 8 safe areas, index.html includes `viewport-fit=cover`; the registry injects the safe-area and full-height shell rules into the project's configured global CSS file. Use the default `SystemBars.insetsHandling = \"css\"` behavior so Android can inject `--safe-area-inset-*` variables."
}
]
}
15 changes: 15 additions & 0 deletions registry/react-capacitor-navigation/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title>Capacitor App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions registry/react-capacitor-navigation/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useRef } from "react"
import { useLocation } from "react-router"
import { setupRouterOutlet } from "@capgo/capacitor-transitions/react"

import Router from "./router"

export default function App() {
const location = useLocation()
const outletRef = useRef<HTMLElement>(null)

useEffect(() => {
if (!outletRef.current) {
return
}

setupRouterOutlet(outletRef.current, {
platform: "auto",
swipeGesture: "auto",
})
}, [])

return (
<cap-router-outlet ref={outletRef}>
<Router key={location.pathname} location={location} />
</cap-router-outlet>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ReactNode } from "react"
import { ChevronLeftIcon } from "lucide-react"
import { useNavigate } from "react-router"
import { setDirection } from "@capgo/capacitor-transitions/react"

import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"

interface CapacitorHeaderProps {
title?: string
children?: ReactNode
className?: string
}

export default function CapacitorHeader({
title,
children,
className,
}: CapacitorHeaderProps) {
const navigate = useNavigate()

function goBack() {
setDirection("back")
navigate(-1)
}

return (
<cap-header
slot="header"
className={cn(
"sticky top-0 z-50 border-b bg-background/80 pt-[var(--safe-area-top)] backdrop-blur-md",
className
)}
>
<div className="relative flex h-12 items-center justify-center px-4">
<Button
type="button"
variant="ghost"
size="icon"
className="absolute left-4"
onClick={goBack}
aria-label="Go back"
>
<ChevronLeftIcon aria-hidden="true" />
</Button>
{title ? <p className="text-sm font-semibold">{title}</p> : null}
{children}
</div>
</cap-header>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HomeIcon, SettingsIcon } from "lucide-react"
import { NavLink } from "react-router"
import { setNavigation } from "@capgo/capacitor-transitions/react"

import { cn } from "@/lib/utils"

const tabs = [
{ to: "/app", icon: HomeIcon, label: "Home" },
{ to: "/app/settings", icon: SettingsIcon, label: "Settings" },
]

export default function CapacitorTabBar() {
return (
<cap-footer slot="footer">
<nav className="border-t bg-background">
<div className="flex pb-[var(--safe-area-bottom)]">
{tabs.map((tab) => (
<NavLink
key={tab.to}
to={tab.to}
end={tab.to === "/app"}
replace
onClick={() => setNavigation("root", "none")}
className={({ isActive }) =>
cn(
"flex h-14 flex-1 flex-col items-center justify-center gap-1 text-xs font-medium transition-colors",
isActive ? "text-primary" : "text-muted-foreground"
)
}
>
<tab.icon className="size-5" aria-hidden="true" />
<span>{tab.label}</span>
</NavLink>
))}
</div>
</nav>
</cap-footer>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useRef } from "react"
import { setupPage } from "@capgo/capacitor-transitions/react"

export function useCapacitorPage<TElement extends HTMLElement = HTMLElement>() {
const pageRef = useRef<TElement>(null)

useEffect(() => {
if (!pageRef.current) {
return
}

return setupPage(pageRef.current)
}, [])

return pageRef
}
18 changes: 18 additions & 0 deletions registry/react-capacitor-navigation/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { BrowserRouter } from "react-router"
import { initTransitions } from "@capgo/capacitor-transitions/react"
import "@capgo/capacitor-transitions"

import App from "./app"
import "./index.css"

initTransitions({ platform: "auto" })

createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
)
37 changes: 37 additions & 0 deletions registry/react-capacitor-navigation/src/pages/app/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ArrowRightIcon } from "lucide-react"
import { useNavigate } from "react-router"
import { setDirection } from "@capgo/capacitor-transitions/react"

import CapacitorTabBar from "@/components/capacitor-tab-bar"
import { Button } from "@/components/ui/button"
import { useCapacitorPage } from "@/hooks/use-capacitor-page"

export default function Home() {
const navigate = useNavigate()
const pageRef = useCapacitorPage()

function goToDetails() {
setDirection("forward")
navigate("/app/details")
}

return (
<cap-page ref={pageRef}>
<cap-content slot="content">
<div className="flex flex-col gap-5 px-6 pb-6 pt-[calc(var(--safe-area-top)+1.5rem)]">
<p className="text-sm text-muted-foreground">Capstart Navigation</p>
<h1 className="text-3xl font-semibold tracking-tight">Welcome</h1>
<p className="text-sm text-muted-foreground">
This screen lives inside the tab navigation.
</p>

<Button className="w-full" onClick={goToDetails}>
Go to details
<ArrowRightIcon aria-hidden="true" />
</Button>
</div>
</cap-content>
<CapacitorTabBar />
</cap-page>
)
}
Loading