Navigation
Native-feeling page transitions and iOS edge-swipe-back, layered over Angular Router / React Router / Vue Router — opt-in and gracefully degrading.
touchcn adds two things to your router’s navigation: animated page transitions (an iOS parallax push/pop, an MD3 shared-axis slide) and an iOS edge-swipe-back gesture. Both are opt-in and layer over the standard router — touchcn ships no router of its own.
Transitions are powered by the browser’s View Transitions API. Where it isn’t supported, navigation still works — it just happens instantly, with no animation.
Setup
Angular
Add the router feature to provideRouter, and the navigation providers to your app config.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideTouchcn } from '@touchcn/angular';
import { provideTouchcnNavigation, withTouchcnViewTransitions } from '@touchcn/angular/navigation';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideTouchcn(),
provideRouter(routes, withTouchcnViewTransitions()),
provideTouchcnNavigation(),
],
};
withTouchcnViewTransitions() wraps Angular’s own withViewTransitions() — it enables the
transition for every router navigation and installs touchcn’s direction hook.
provideTouchcnNavigation() provides the direction service the hook reads and wires the
swipe-back gesture.
React
touchcn’s React navigation builds on React Router’s data router (createBrowserRouter +
RouterProvider), which is what runs document.startViewTransition for you. Mount the two hooks
once in a layout route, and opt each navigation into a transition with viewTransition.
import { createBrowserRouter, Link, Outlet, RouterProvider } from 'react-router';
import { useSwipeBack, useTouchcnNavigation } from '@touchcn/react';
function RootLayout() {
useTouchcnNavigation(); // direction tracking → data-tcn-nav
useSwipeBack(); // iOS edge-swipe-back
return <Outlet />;
}
const router = createBrowserRouter([
{
element: <RootLayout />,
children: [
{ path: '/', element: <Home /> },
{ path: '/details', element: <Details /> },
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
Opt a navigation into a transition with the viewTransition prop (or the useNavigate option):
<Link to="/details" viewTransition>Details</Link>
Vue
touchcn’s Vue navigation layers over Vue Router. Call the two composables once in a layout
component mounted under the one that calls provideTouchcn() — useTouchcnNavigation() runs each
navigation inside document.startViewTransition from a router guard and stamps data-tcn-nav;
useSwipeBack() adds the iOS edge-swipe-back gesture.
<script setup lang="ts">
import { RouterView } from 'vue-router';
import { useSwipeBack, useTouchcnNavigation } from '@touchcn/vue';
useTouchcnNavigation(); // direction tracking → data-tcn-nav, view-transition guard
useSwipeBack(); // iOS edge-swipe-back
</script>
<template>
<RouterView />
</template>
Unlike React’s per-Link viewTransition opt-in, the Vue hook animates every navigation. Opt a
route out of the transition with meta.skipViewTransition — an instant swap when either the source
or target route carries it (used for chrome-less embeds loaded directly, never via an in-app link):
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/details', component: Details },
{ path: '/embed/:id', component: Embed, meta: { skipViewTransition: true } },
],
});
How it works
- Tap / programmatic navigation animates through the View Transitions API, driven by your
router’s built-in view-transition support. touchcn stamps a
data-tcn-nav="forward" | "back"attribute on<html>for the duration of each transition; the stylesheet forks the::view-transition-*(root)animation per platform and per direction. - Direction is derived from the router’s navigation stream: a back navigation (browser/OS back
button,
Location.back()/navigate(-1), or a swipe) isback; everything else isforward. - Swipe-back can’t be a view transition (it follows your finger), so it’s hand-rolled: dragging from the left edge translates the current page over a dimmed parallax underlay, and on release it either completes the slide and triggers the router back (with the transition suppressed to an instant swap) or springs back.
Direction semantics
| Navigation | Direction | data-tcn-nav |
|---|---|---|
Link / navigate / imperative push |
forward | forward |
Browser/OS back, Location.back() / navigate(-1) |
back | back |
| Edge-swipe-back | back | (suppressed — the drag already animated) |
- iOS: forward slides the incoming page in from the trailing edge while the outgoing page parks ~30% toward the leading edge and dims; back mirrors it. This matches the swipe-back drag, so a tap and a gesture look the same.
- Material: an MD3 shared-axis (X) transition — a short lateral slide paired with a fade, direction-aware. Opaque surfaces only (no glass).
Browser support & degradation
Same-document view transitions are required for the tap/programmatic animation:
| Engine | Animated transitions | Notes |
|---|---|---|
| Chrome / Android WebView 111+ | ✅ | Full support. |
| Safari / iOS WebView 18+ | ✅ | Full support. |
| Older Chrome / Safari, Firefox (no same-document VT) | ⚠️ Degrades | Navigation is instant — fully functional, just un-animated. data-tcn-nav is still set. |
The swipe-back gesture itself has no such requirement — it’s plain pointer + transform — so it works on any modern engine, on the iOS theme.
Reduced motion
With prefers-reduced-motion: reduce, the platform slide/parallax animations are replaced by a
plain cross-fade, and the swipe-back keeps its functional page-follow drag but drops the decorative
parallax underlay and page shadow.