---
title: Navigation
description: Native-feeling page transitions and iOS edge-swipe-back, layered over Angular Router / React Router / Vue Router — opt-in and gracefully degrading.
sidebar:
  label: Navigation
  icon: arrow-left-right
---

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](https://developer.mozilla.org/en-US/docs/Web/API/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.

```ts app.config.ts
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.

:::note
Pass `withTouchcnViewTransitions({ skipInitialTransition: true })` to keep the very first page load
un-animated. Tune or disable the gesture with `provideTouchcnNavigation({ swipeBack: { edgeWidth: 24 } })`
or `provideTouchcnNavigation({ swipeBack: false })`.
:::

### 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`.

```tsx App.tsx
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):

```tsx
<Link to="/details" viewTransition>Details</Link>
```

:::note
`useSwipeBack()` accepts `{ disabled, edgeWidth }`. Back navigations triggered by the gesture or by
`navigate(-1)` are automatically classified as `back`.
:::

### 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.

```vue RootLayout.vue
<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):

```ts router.ts
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/details', component: Details },
    { path: '/embed/:id', component: Embed, meta: { skipViewTransition: true } },
  ],
});
```

:::note
`useSwipeBack()` accepts `{ disabled, edgeWidth }`, same as React. Back navigations from the gesture
or `router.back()` are automatically classified as `back`.
:::

## 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.

:::note
**A v1 limitation:** the underlay revealed behind the dragged page is a neutral dimmed surface with
parallax, **not** a live snapshot of the previous page — you won't see the actual previous screen
until the back navigation commits. Rendering the real previous page during the drag needs a
page/stack cache that keeps the prior view mounted; that's a planned follow-up.
:::

## 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.
