4 min read
On this page

Dynamic Routes & Params

Overview

Static routes cover pages with fixed URLs like /about or /pricing. Most applications also need dynamic routes where part of the URL changes: /blog/my-first-post, /users/42, /docs/getting-started/installation. SvelteKit handles this with bracket syntax in folder names. A folder named [slug] matches any single path segment. A folder named [...rest] matches any number of segments.

Dynamic Segments with [slug]

Square brackets in a directory name create a dynamic parameter. The parameter name is whatever you put inside the brackets.

src/routes/
  blog/
    [slug]/
      +page.svelte      → /blog/hello-world, /blog/my-post, etc.
      +page.server.ts
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ params }) => {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });

  if (!post) {
    error(404, 'Post not found');
  }

  return { post };
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<article>
  <h1>{data.post.title}</h1>
  <time>{data.post.publishedAt}</time>
  {@html data.post.content}
</article>

When someone visits /blog/hello-world, params.slug is "hello-world". The name inside the brackets becomes the key in the params object.

Multiple Dynamic Segments

Routes can have multiple dynamic segments at different levels.

src/routes/
  [org]/
    [repo]/
      +page.svelte          → /sveltejs/kit, /facebook/react
      issues/
        [id]/
          +page.svelte      → /sveltejs/kit/issues/123
// src/routes/[org]/[repo]/issues/[id]/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  // params.org = "sveltejs"
  // params.repo = "kit"
  // params.id = "123"
  const issue = await fetchIssue(params.org, params.repo, params.id);
  return { issue };
};

Catch-All Routes with [...rest]

The spread syntax matches any number of remaining path segments, including nested paths with slashes.

src/routes/
  docs/
    [...path]/
      +page.svelte      → /docs/intro
                         → /docs/getting-started/installation
                         → /docs/api/components/button
// src/routes/docs/[...path]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ params }) => {
  // /docs/getting-started/installation → params.path = "getting-started/installation"
  const segments = params.path.split('/');

  const doc = await loadDocument(segments);
  if (!doc) {
    error(404, 'Documentation page not found');
  }

  return {
    doc,
    breadcrumbs: segments
  };
};
<!-- src/routes/docs/[...path]/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<nav aria-label="breadcrumb">
  <a href="/docs">Docs</a>
  {#each data.breadcrumbs as crumb, i}
    <span>/</span>
    <a href="/docs/{data.breadcrumbs.slice(0, i + 1).join('/')}">{crumb}</a>
  {/each}
</nav>

<article>
  {@html data.doc.html}
</article>

Catch-all routes are useful for CMS-driven content, documentation sites, and any page structure where the depth is not fixed.

Catch-All with a Default

A catch-all route also matches the parent path when the rest parameter is empty. Visiting /docs sets params.path to "".

// src/routes/docs/[...path]/+page.server.ts
export const load: PageServerLoad = async ({ params }) => {
  const path = params.path || 'index';
  const doc = await loadDocument(path.split('/'));
  return { doc };
};

Layout Groups Without URL Segments

Route groups with (parentheses) were covered in the previous section for layout purposes. They also matter for dynamic routing because they let you apply different layouts to different dynamic routes without adding segments.

src/routes/
  (public)/
    +layout.svelte          → public layout
    blog/
      [slug]/
        +page.svelte        → /blog/my-post (public layout)
  (admin)/
    +layout.svelte          → admin layout
    blog/
      [slug]/
        edit/
          +page.svelte      → /blog/my-post/edit (admin layout)

The blog post view uses a public layout with a reading-focused design. The edit page uses an admin layout with a toolbar and sidebar. The URL hierarchy stays clean.

Param Matchers

By default, a dynamic segment matches any string. Matchers let you constrain what a parameter can be.

// src/params/integer.ts
import type { ParamMatcher } from '@sveltejs/kit';

export const match: ParamMatcher = (param) => {
  return /^\d+$/.test(param);
};

Use the matcher by adding =matcherName inside the brackets.

src/routes/
  users/
    [id=integer]/
      +page.svelte      → /users/42 matches, /users/alice does not
// src/params/slug.ts
import type { ParamMatcher } from '@sveltejs/kit';

export const match: ParamMatcher = (param) => {
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(param);
};
src/routes/
  blog/
    [slug=slug]/
      +page.svelte      → /blog/hello-world matches
                         → /blog/Hello_World does not match

Matchers are useful when you have multiple dynamic routes at the same level that need to be disambiguated.

src/routes/
  [username]/
    +page.svelte            → /alice, /bob (user profiles)
  [id=integer]/
    +page.svelte            → /42 (item by ID)

Without the matcher, SvelteKit would not know whether /42 is a username or an ID. The matcher makes the intent explicit: if it is all digits, route to the item page; otherwise, treat it as a username.

Optional Parameters

SvelteKit supports optional parameters using double brackets [[param]]. The route matches with or without the segment.

src/routes/
  blog/
    [[page]]/
      +page.svelte      → /blog (page 1) and /blog/2, /blog/3, etc.
// src/routes/blog/[[page]]/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  const page = params.page ? parseInt(params.page) : 1;
  const perPage = 10;

  const posts = await db.post.findMany({
    skip: (page - 1) * perPage,
    take: perPage,
    orderBy: { createdAt: 'desc' }
  });

  const total = await db.post.count();

  return {
    posts,
    page,
    totalPages: Math.ceil(total / perPage)
  };
};
<!-- src/routes/blog/[[page]]/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<h1>Blog</h1>

{#each data.posts as post}
  <article>
    <a href="/blog/{post.slug}">{post.title}</a>
  </article>
{/each}

<nav>
  {#if data.page > 1}
    <a href="/blog/{data.page - 1}">Previous</a>
  {/if}
  {#if data.page < data.totalPages}
    <a href="/blog/{data.page + 1}">Next</a>
  {/if}
</nav>

Accessing Params in Page Components

Params are available in load functions through the params argument. In page components, params come through the loaded data or through the $page store.

<!-- Using data from the load function (preferred) -->
<script lang="ts">
  let { data } = $props();
</script>

<h1>{data.post.title}</h1>
<!-- Using the page store directly (when you need the raw param) -->
<script lang="ts">
  import { page } from '$app/stores';
</script>

<p>Current slug: {$page.params.slug}</p>

The load function approach is preferred because it lets you validate and transform parameters before the component sees them. The $page.params approach gives you the raw string, which is useful for things like highlighting the active nav item.

Common Pitfalls

  • Ambiguous routes without matchers: If you have [username]/+page.svelte and [id]/+page.svelte at the same level, SvelteKit cannot determine which route /42 should match. Use param matchers to disambiguate.
  • Forgetting that params are always strings: params.id is "42", not 42. Always parse numeric parameters with parseInt() or Number().
  • Catch-all routes swallowing everything: A [...rest] route at src/routes/[...rest]/ matches every URL in your application. Place catch-all routes as deep in the hierarchy as possible to avoid conflicts.
  • Not handling missing params in optional routes: With [[page]], params.page is undefined when the segment is absent. Always provide a default value.
  • Relying on param format without validation: Even with matchers, always validate params in your load function. A matcher ensures the route matches, but your database query still needs to handle the case where no record exists for that param value.

Key Takeaways

  • [slug] creates a dynamic segment that matches any single path part. The bracket name becomes the key in params.
  • [...rest] creates a catch-all route that matches any number of segments, including nested paths with slashes.
  • [[param]] creates an optional parameter. The route matches with or without that segment.
  • Param matchers in src/params/ constrain what values a dynamic segment accepts. Use them to disambiguate routes at the same level.
  • Route groups with (parentheses) organize layouts without affecting URLs, which is especially useful when different dynamic routes need different layouts.
  • Always validate and parse params in load functions. Params are raw strings from the URL, and your code must handle invalid or missing values gracefully.