logo
  • Guide
  • API
  • Blog
  • English
    • 简体中文
    • English
    • @esmx/core
      Esmx
      App
      RenderContext
      ModuleConfig
      PackConfig
      ManifestJson
      @esmx/router
      Router
      Route
      Route Configuration
      RouterLink
      Navigation Guards
      Dynamic Route Matching
      Nested Routes
      Programmatic Navigation
      Scroll Behavior
      Layer Routing
      MicroApp
      Error Types
      Types Reference
      @esmx/router-vue
      RouterPlugin
      Composables
      Components
      Type Augmentation
      @esmx/router-react
      Micro-App Integration
      Hooks & Context
      Components
      SSR
      App
      @esmx/rspack
      @esmx/rspack-vue
      @esmx/rspack-react

      Last Updated: 4/7/2026, 2:16:07 AM

      Previous pageProgrammatic NavigationNext pageLayer Routing

      #Scroll Behavior

      When navigating between routes, @esmx/router automatically manages scroll position to match user expectations. Pushing to a new page scrolls to the top; going back restores the previous scroll position. This mirrors how traditional multi-page websites behave.

      #Default Behavior

      The router handles scrolling differently based on the navigation type:

      • push: Scrolls to top (0, 0)
      • replace: Scrolls to top (0, 0)
      • back: Restores saved scroll position
      • forward: Restores saved scroll position
      • go(n): Restores saved scroll position
      • pushWindow: Handled by browser
      • replaceWindow: Handled by browser
      await router.push('/new-page');
      
      await router.back();

      This works out of the box with no configuration needed.

      #How Scroll Positions Are Saved

      When leaving a page (via push, replace, or history navigation), the router saves the current scroll position using two mechanisms:

      1. In-memory map: A Map<string, ScrollPosition> keyed by the page's full URL
      2. History state: The position is also stored in history.state under the __scroll_position_key property
      const scrollPosition = { left: window.scrollX, top: window.scrollY };
      
      scrollPositions.set(currentUrl, scrollPosition);
      
      history.replaceState({
        ...history.state,
        __scroll_position_key: scrollPosition
      }, '');

      Storing in history.state means scroll positions survive page refreshes — when the user refreshes and then navigates back, the correct scroll position can still be restored.

      #Manual Scroll Restoration

      The router sets history.scrollRestoration = 'manual' automatically. This tells the browser not to attempt its own scroll restoration, leaving full control to the router.

      This is configured during the confirm phase of navigation — you don't need to set it yourself.

      #Keeping Scroll Position

      Sometimes you don't want navigation to scroll to the top. For example, when switching tabs or filtering content, the user expects to stay where they are:

      await router.push({
        path: '/dashboard',
        query: { tab: 'settings' },
        keepScrollPosition: true
      });

      When keepScrollPosition is set to true:

      • The page does not scroll to top
      • The current scroll position is not saved (since we're staying at the same position)
      • The __keepScrollPosition flag is stored in history.state

      This flag is also checked during back/forward navigation — if the target history entry was created with keepScrollPosition: true, scroll restoration is skipped.

      #Scroll to Element

      The scroll system supports scrolling to a specific element on the page using a CSS selector:

      import { scrollToPosition } from '@esmx/router';
      
      scrollToPosition({ el: '#section-title' });
      
      scrollToPosition({
        el: '#section-title',
        top: -80,
        behavior: 'smooth'
      });
      
      const element = document.querySelector('.target');
      scrollToPosition({ el: element });

      The el property accepts:

      • A CSS selector string (e.g., '#my-id', '.my-class', '[data-section]')
      • A DOM Element reference
      Tip

      If you need to scroll to an element after navigation, use the afterEach hook:

      router.afterEach((to) => {
        if (to.hash) {
          setTimeout(() => {
            scrollToPosition({ el: to.hash });
          }, 100); // wait for DOM to update
        }
      });

      #Layer Routes and Scroll

      Routes opened as layers (via pushLayer or createLayer) skip scroll handling entirely. Since layers render in an overlay on top of the current page, scrolling the background page would be disruptive:

      await router.pushLayer('/confirm-dialog');

      This behavior is built into the router's confirm phase — scroll logic is skipped when router.isLayer is true.

      #Scroll Position Flow

      Here's the complete flow of how scroll is handled during different navigation types:

      #push / replace

      1. Save current scroll position for the current URL
      2. Perform navigation (update history, mount component)
      3. Scroll to (0, 0) — unless keepScrollPosition is true

      #back / forward / go

      1. Save current scroll position for the current URL
      2. Perform navigation (history popstate fires)
      3. Wait for DOM update (nextTick)
      4. Check if history.state has __keepScrollPosition flag
         → If yes: skip scroll restoration
         → If no: restore saved scroll position for the new URL
           → Falls back to (0, 0) if no saved position exists

      #Window Navigation (pushWindow / replaceWindow)

      1. Full browser navigation — scroll handled by browser natively

      #Summary

      • Scroll to top on push/replace: Enabled by default. Pass keepScrollPosition: true to disable.
      • Restore scroll on back/forward: Enabled by default. Uses saved positions automatically.
      • Browser scroll restoration: Disabled ('manual'). Set automatically by router.
      • Layer scroll handling: Skipped. Automatic for layer routes.
      • Persist across page refresh: Via history.state. Automatic.