Framework-agnostic page transitions for Capacitor apps. iOS-style navigation without opinions.
- Framework Agnostic - Works with React, Vue, Angular, Svelte, Solid, and any other framework
- iOS & Android Animations - Platform-appropriate transitions out of the box
- Web Animations API - Smooth, GPU-accelerated animations
- View Transitions API - Progressive enhancement for supporting browsers
- No Design Opinions - Just transition logic, you bring your own styles
- Coordinated Transitions - Header, content, and footer animate together
- Page Caching - Keep pages in DOM for instant back navigation
- Lifecycle Hooks - willEnter, didEnter, willLeave, didLeave events
npm install @capgo/transitions<cap-router-outlet platform="auto">
<cap-page>
<cap-header slot="header">
<h1>My Page</h1>
</cap-header>
<cap-content slot="content">
<p>Page content here</p>
</cap-content>
<cap-footer slot="footer">
<nav>Tab bar</nav>
</cap-footer>
</cap-page>
</cap-router-outlet>import '@capgo/transitions';
// Navigate programmatically
const outlet = document.querySelector('cap-router-outlet');
outlet.push(newPageElement);
outlet.pop();
outlet.setRoot(newRootElement);import { useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { initTransitions, setDirection, setupPage, setupRouterOutlet } from '@capgo/transitions/react';
import '@capgo/transitions';
// Initialize once at app startup
initTransitions({ platform: 'auto' });
function App() {
const outletRef = useRef<HTMLElement>(null);
useEffect(() => {
if (outletRef.current) {
setupRouterOutlet(outletRef.current, { platform: 'auto' });
}
}, []);
return (
<cap-router-outlet ref={outletRef}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/details/:id" element={<DetailsPage />} />
</Routes>
</cap-router-outlet>
);
}
function HomePage() {
const navigate = useNavigate();
const pageRef = useRef<HTMLElement>(null);
useEffect(() => {
if (pageRef.current) {
return setupPage(pageRef.current, {
onDidEnter: () => console.log('entered'),
});
}
}, []);
const goToDetails = (id: number) => {
setDirection('forward');
navigate(`/details/${id}`);
};
return (
<cap-page ref={pageRef}>
<cap-header slot="header">
<h1>Home</h1>
</cap-header>
<cap-content slot="content">
<button onClick={() => goToDetails(1)}>Go to Details</button>
</cap-content>
<cap-footer slot="footer">
<nav>Tab bar</nav>
</cap-footer>
</cap-page>
);
}<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { initTransitions, setDirection, setupPage, setupRouterOutlet } from '@capgo/transitions/vue';
import '@capgo/transitions';
// Initialize once
initTransitions({ platform: 'auto' });
const router = useRouter();
const outletRef = ref(null);
const pageRef = ref(null);
let cleanup;
onMounted(() => {
if (outletRef.value) {
setupRouterOutlet(outletRef.value, { platform: 'auto' });
}
if (pageRef.value) {
cleanup = setupPage(pageRef.value, {
onDidEnter: () => console.log('entered'),
});
}
});
onUnmounted(() => cleanup?.());
const goToDetails = (id) => {
setDirection('forward');
router.push(`/details/${id}`);
};
</script>
<template>
<cap-router-outlet ref="outletRef">
<cap-page ref="pageRef">
<cap-header slot="header">
<h1>Home</h1>
</cap-header>
<cap-content slot="content">
<button @click="goToDetails(1)">Go to Details</button>
</cap-content>
</cap-page>
</cap-router-outlet>
</template><script>
import { routerOutlet, page, setDirection } from '@capgo/transitions/svelte'
import '@capgo/transitions'
function navigate(to, direction = 'forward') {
setDirection(direction)
// Use your router's navigate function
}
</script>
<cap-router-outlet use:routerOutlet>
<cap-page use:page={{ onDidEnter: () => console.log('entered') }}>
<cap-header slot="header">
<h1>Home</h1>
</cap-header>
<cap-content slot="content">
<button on:click={() => navigate('/details/1')}>Go to Details</button>
</cap-content>
</cap-page>
</cap-router-outlet>import { onMount, onCleanup } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { initTransitions, setDirection, setupPage, setupRouterOutlet } from '@capgo/transitions/solid';
import '@capgo/transitions';
// Initialize once
initTransitions({ platform: 'auto' });
function HomePage() {
const navigate = useNavigate();
let pageRef;
onMount(() => {
if (pageRef) {
const cleanup = setupPage(pageRef, {
onDidEnter: () => console.log('entered'),
});
onCleanup(cleanup);
}
});
const goToDetails = (id) => {
setDirection('forward');
navigate(`/details/${id}`);
};
return (
<cap-page ref={pageRef}>
<cap-header slot="header">
<h1>Home</h1>
</cap-header>
<cap-content slot="content">
<button onClick={() => goToDetails(1)}>Go to Details</button>
</cap-content>
</cap-page>
);
}// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import '@capgo/transitions';
@Component({
selector: 'app-root',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<cap-router-outlet #outlet platform="auto">
<router-outlet></router-outlet>
</cap-router-outlet>
`,
})
export class AppComponent {}
// home.component.ts
@Component({
selector: 'app-home',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<cap-page>
<cap-header slot="header">
<h1>Home</h1>
</cap-header>
<cap-content slot="content">
<button (click)="goToDetails(1)">Go to Details</button>
</cap-content>
</cap-page>
`,
})
export class HomeComponent {
constructor(private router: Router) {}
goToDetails(id: number) {
this.router.navigate(['/details', id]);
}
}Container for page transitions.
| Attribute | Type | Default | Description |
|---|---|---|---|
platform |
'ios' | 'android' | 'auto' |
'auto' |
Animation style |
duration |
number |
Platform default | Animation duration in ms |
keep-in-dom |
boolean |
true |
Keep pages in DOM after navigating away |
max-cached |
number |
10 |
Maximum pages to keep cached |
Methods:
push(element, config?)- Navigate forward to new pagepop(config?)- Navigate backsetRoot(element, config?)- Replace navigation stack
Page container with header/content/footer slots.
Events:
cap-will-enter- Before page becomes visiblecap-did-enter- After page becomes visiblecap-will-leave- Before page leavescap-did-leave- After page leaves
Header container. Use with slot="header" inside <cap-page>.
Main scrollable content area. Use with slot="content".
| Attribute | Type | Default | Description |
|---|---|---|---|
fullscreen |
boolean |
false |
Content scrolls behind header |
scroll-x |
boolean |
true |
Enable horizontal scroll |
scroll-y |
boolean |
true |
Enable vertical scroll |
Footer container. Use with slot="footer".
| Direction | Description |
|---|---|
'forward' |
Push animation (iOS: slide from right) |
'back' |
Pop animation (iOS: slide to right) |
'root' |
Replace animation (fade) |
'none' |
No animation |
All framework bindings export these helper functions:
// Initialize the transition system
initTransitions({ platform: 'auto' });
// Set the direction for the next navigation
setDirection('forward' | 'back' | 'root' | 'none');
// Set up a router outlet element
setupRouterOutlet(element, options);
// Set up a page element with lifecycle callbacks (returns cleanup function)
setupPage(element, { onWillEnter, onDidEnter, onWillLeave, onDidLeave });
// Create a transition-aware navigate function
const transitionNavigate = createTransitionNavigate(navigate);
transitionNavigate('/path', 'forward');For advanced programmatic control:
import { createTransitionController } from '@capgo/transitions';
const controller = createTransitionController({
platform: 'auto',
duration: 400,
useViewTransitions: true,
});
// Navigate
await controller.push(element, { direction: 'forward' });
await controller.pop({ direction: 'back' });
await controller.setRoot(element, { direction: 'root' });
// Lifecycle hooks
controller.registerLifecycle('page-id', {
onWillEnter: (event) => console.log('Will enter', event),
onDidEnter: (event) => console.log('Did enter', event),
onWillLeave: (event) => console.log('Will leave', event),
onDidLeave: (event) => console.log('Did leave', event),
});- Modern browsers with Web Animations API support
- View Transitions API (Chrome 111+, Edge 111+, Safari 18+) for enhanced transitions
- Graceful fallback for older browsers
This library is intentionally unopinionated about styling:
- No CSS included - You bring your own styles
- No design system - Works with any UI library or custom styles
- Just transitions - Focus on smooth page navigation
- Framework agnostic - Use with React, Vue, Angular, Svelte, Solid, or vanilla JS
The goal is to provide Ionic-quality page transitions without Ionic's design system or framework lock-in.
See the /examples directory for complete examples:
react-app- React with React Routervue-app- Vue 3 with Vue Routerangular-app- Angular with Angular Routersvelte-app- Svelte 5solid-app- Solid with Solid Routertanstack-app- React with TanStack Router
MIT