Vue – Routing with Vue Router

December 2, 20254 min readUpdated 8/24/2026

Vue Router is the official router. It is not part of Vue core, but it is written by the Vue team and it is what essentially every Vue application uses.

Creating the router

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, saved) {
    // The feed is its own scroll-snap container; restoring a window scroll
    // position into it would fight the snapping.
    if (to.name === "feed") return false;
    return saved ?? { top: 0 };
  },

createWebHistory() gives real URLs — /explore. The alternative, createWebHashHistory(), gives /#/explore, which needs no server configuration but is worse for SEO and looks dated. Use history mode and configure the host; lesson 27 covers the one rewrite rule it needs.

Then install it as a plugin:

createApp(App).use(createPinia()).use(router).mount("#app");

Routes

const routes = [
  { path: "/explore", name: "explore", component: ExploreView },
  { path: "/r/:slug", name: "reel", component: ReelView },
];

Always name your routes. A path is a string repeated at every link site; a name is a reference. Renaming /r/:slug to /reels/:slug is one edit if links are by name, and a grep-and-pray if they are by path.

<RouterLink :to="{ name: 'reel', params: { slug: reel.slug } }">Watch</RouterLink>
<RouterLink :to="{ name: 'explore', query: { q: 'buzzer' } }">Search</RouterLink>

<RouterView />

<RouterLink> renders an <a> with a real href — so middle-click, right-click and open-in-new-tab all behave — and intercepts the click to navigate without a page load. It also adds router-link-active and router-link-exact-active classes, which you can rename per link:

            :active-class="item.exact ? 'is-ancestor' : 'is-current'"
            exact-active-class="is-current"

<RouterView /> is where the matched component renders.

Route params

import { useRoute, useRouter } from "vue-router";

const route = useRoute();     // the CURRENT route -- reactive, read-only
const router = useRouter();   // the router instance -- for navigating

const slug = route.params.slug;

Two different objects and the names are easy to confuse. route is where you are; router is how you move.

route.params is reactive, and the component is not remounted between two params of the same route. Navigating from /r/one to /r/two reuses the component, so anything in onMounted does not run again. Read the param in a watcher with immediate: true, and both the first load and every later change are handled by the same code path.

Programmatic navigation:

router.push({ name: "explore", query: { q: term } });   // adds a history entry
router.replace({ name: "feed" });                       // replaces the current one
router.back();

Nested routes and layouts

This is the most useful structural idea in the router, and the demo application is built on it. A route with children renders its own component, which contains a <RouterView /> where the child renders:

    path: "/",
    component: PublicLayout,
    children: [
      {
        path: "",
        name: "feed",
        component: () => import("../views/public/FeedView.vue"),
        meta: { title: "Feed", chrome: false },
      },
      {
        path: "explore",
        name: "explore",
        component: () => import("../views/public/ExploreView.vue"),
        meta: { title: "Explore" },
      },
      {

PublicLayout holds the navbar and the container; every child renders inside it. AdminLayout does the same with a sidebar. One application serves a public site and an admin console, split by layout rather than by build — which is far less scaffolding than two apps, and means a single npm run build.

An empty child path (path: "") is the index route for its parent.

Lazy loading

Swap the component for a function that imports it:

component: () => import("../views/public/ExploreView.vue"),

Vite gives that route its own chunk, fetched on first visit. In the demo application's build the dashboard — which pulls in Chart.js — is 197 kB, and it is not downloaded by anyone who never opens the admin area.

The feed is deliberately not lazy, and the comment in the source says why: it is the landing route, so a lazy chunk there just adds a round trip before the first pixel. Lazy-load everything except the route people land on.

The 404

    path: "/:pathMatch(.*)*",
    name: "not-found",
    component: () => import("../views/NotFoundView.vue"),
    meta: { title: "Not found" },
  },

The syntax is a named param with a custom regex and a repeat modifier. Put it last — routes match in order.

Route meta

Arbitrary data attached to a route, which is how the application avoids hard-coding route names in its layouts:

const showChrome = computed(() => route.meta.chrome !== false);

The feed is a full-bleed scroll-snap pager that owns the viewport, so it declares meta: { chrome: false } and the layout reads it. Adding another full-bleed route later is one line in the route table rather than an edit to a growing || chain.

meta is also what guards read, which is the next lesson.

scrollBehavior

  scrollBehavior(to, from, saved) {
    // The feed is its own scroll-snap container; restoring a window scroll
    // position into it would fight the snapping.
    if (to.name === "feed") return false;
    return saved ?? { top: 0 };
  },

Restore the saved position on back/forward, and go to the top otherwise. The feed opts out, because it is its own scroll container and restoring a window position into it fights the snapping.

Next: Route Guards and Navigation.