Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

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) is back; everything else is forward.
  • 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.

Last updated on July 24, 2026

Was this page helpful?