Routing

Stack Navigation

Mobile-style stack navigation; kept pages, the transition animation and swiping back.

Stack navigation — stack

By default, when you leave a page the old page is thrown away, and it is recreated when you come back. In mobile apps, though, the expected behaviour is a stack: going forward keeps the previous page with its state, its scroll position and its form, and going back brings back the same instance. stack turns this on:

app.useRouter({
  routes,
  mode: "history",
  stack: {
    retain: true,        // keep previous pages hidden in the DOM (default false: in memory, detached from the DOM)
    depth: 5,            // how many previous pages to keep at most (default 5; the oldest page past the limit is dropped)
    persist: false,      // write the stack's addresses to sessionStorage (default false)
    animation: "slide",  // 'none' (default) | 'slide' | a custom function
    duration: 300,       // 'slide' duration (ms)
    swipeBack: true,     // go back by swiping right from the left edge of the screen (off by default)
  },
});
app.useRouter({
  routes,
  mode: "history",
  stack: {
    retain: true,        // keep previous pages hidden in the DOM (default false: in memory, detached from the DOM)
    depth: 5,            // how many previous pages to keep at most (default 5; the oldest page past the limit is dropped)
    persist: false,      // write the stack's addresses to sessionStorage (default false)
    animation: "slide",  // 'none' (default) | 'slide' | a custom function
    duration: 300,       // 'slide' duration (ms)
    swipeBack: true,     // go back by swiping right from the left edge of the screen (off by default)
  },
});

stack: true turns it on with the default values. If the option is not given, the behaviour is as described above and nothing changes. In shell mode it has no effect, because there is no history.

The rules:

app.router.stack gives the current list of the stack: [{ index, uri, current, retained }]. Use it for work such as showing the previous page's name on a back button. With persist: true this list comes back after a page reload too; on reload the previous pages are not recreated, they are created as you go back.

The transition animation. animation: 'slide' slides the new page in from the right when going forward, and slides the current page out to the right when going back; meanwhile the other page stays underneath. The animation works independently of the retain setting: with retain: false, the page being left stays in the DOM until the animation finishes and is detached after that. If the user has chosen "reduce motion" in the system, the animation is skipped. To write your own animation, give a function; the two pages arrive positioned on top of each other, and the styles are rolled back when the function finishes:

app.useRouter({
  routes,
  stack: {
    retain: true,
    animation: async ({ direction, entering, leaving }) => {
      await Promise.all([
        entering?.animate([{ opacity: 0 }, { opacity: 1 }], 200).finished,
        leaving?.animate([{ opacity: 1 }, { opacity: 0 }], 200).finished,
      ]);
    },
  },
});
app.useRouter({
  routes,
  stack: {
    retain: true,
    animation: async ({ direction, entering, leaving }) => {
      await Promise.all([
        entering?.animate([{ opacity: 0 }, { opacity: 1 }], 200).finished,
        leaving?.animate([{ opacity: 1 }, { opacity: 0 }], 200).finished,
      ]);
    },
  },
});

In a navigation where the stack animation runs, the pages' own transitions (motif.options.transition, see Styling and Animation) do not play; only the stack animation does the transition.

The pages' own transitions. If animation is not given, the stack uses the pages' own enter/leave transitions, and there is no need to define a stack animation separately. The order is the same as with the stack off: first the leave of the page being left finishes, then the incoming page enters.

.page-enter-active { animation: slide-from-right .3s ease; }
.page-leave-active { animation: slide-to-left .3s ease forwards; }
[data-nav-direction="back"].page-enter-active { animation-name: slide-from-left; }
[data-nav-direction="back"].page-leave-active { animation-name: slide-to-right; }

Swiping back. With swipeBack: true, a horizontal touch starting from the left edge of the screen (24px by default) slides the page along with the finger. For the previous page to show underneath while dragging, retain: true is needed; with retain: false the previous page is not in the DOM, so the page background shows underneath, and going back still works on release. Passing 35% of the width (threshold) or a quick flick goes back; otherwise the page settles back into place. If a guard cancels going back, the page slides back and the address is kept. When going back by swiping, the drag is the animation itself; the pages' own transitions do not play. Settings: swipeBack: { edge: 24, threshold: 0.35 }. The touch listener becomes active only on a touch that starts from the edge; it does not affect the page's vertical scrolling or content that scrolls horizontally.

Next step

To learn how services are registered and injected, go on to Introduction to Dependency Injection.