Skip to content

Treat layout pages like components when using named slots #627

@Aias

Description

@Aias

Let's say I have a _layout.svelte like the following:

<slot name="layout-pre"></slot>

<div id="intermediate">Some other intermediate stuff.</div>

<slot></slot>

And now I have a child view (let's call it homepage.svelte which inherits that layout, which puts some stuff into the named slot, and the rest into the default slot:

<h1 slot="layout-pre">This should come before #intermediate.</h1>

<!-- When rendered, the div from above should get inserted here from the layout:
<div id="intermediate">Some other intermediate stuff.</div>
-->

<main>Since this doesn't use a named slot, it'll just get inserted into the unnamed slot above.</main>

Right now this is not allowed, and the following error is thrown: Element with a slot='...' attribute must be a descendant of a component or custom element. Are there technical or usage reasons why we wouldn't want to treat a layout like any other component with respect to slots?

Activity

joakim

joakim commented on Oct 12, 2019

@joakim
buhrmi

buhrmi commented on Nov 4, 2019

@buhrmi

I guess it doesn't work because the Svelte compiler needs to know at compile-time which parent-component the slots will end up in.

jdgaravito

jdgaravito commented on Feb 6, 2020

@jdgaravito
buhrmi

buhrmi commented on Feb 6, 2020

@buhrmi

Well, there's nothing keeping us from writing our pages like

<Layout>
  <h1 slot="above">...</h1>
  <main>derp derp lalalala</main>
</Layout>
silllli

silllli commented on Feb 14, 2020

@silllli
jdgaravito

jdgaravito commented on Feb 14, 2020

@jdgaravito
Bandit

Bandit commented on Mar 14, 2020

@Bandit
zakaria-chahboun

zakaria-chahboun commented on Jul 4, 2020

@zakaria-chahboun

You have to do it in a regular way, by creating a custom layout: (_myLayout.svelte)
and include it in your index page.

mrmaglet

mrmaglet commented on Feb 1, 2021

@mrmaglet

Can someone show how to include a _customLayout.svelte as zakaria-chahboun suggests?

zakaria-chahboun

zakaria-chahboun commented on Feb 8, 2021

@zakaria-chahboun

Can someone show how to include a _customLayout.svelte as zakaria-chahboun suggests?

Example:

We have a route called test.
So we just create a folder named test, and inside of this folder we have our svelte files,
One is our router and the second is our custom layout, like so:

  • test
    • [id].svelte
    • _customLayout.svelte

The _customLayout.svelte is like this:

<slot name="sidebar"></slot>
<slot name="profile"></slot>

And the [id].svelte is like this:

<script context="module">
  export async function preload(page) {
    const { id } = page.params;
    return {id};
}
</script>

<script>
    import customLayout from "./_customLayout.svelte";
</script>

<customLayout>
    <div slot="sidebar">test</div>
</customLayout>
mquandalle

mquandalle commented on Feb 17, 2021

@mquandalle
transferred this issue fromsveltejs/sapperon Mar 24, 2021
Nick-Mazuk

Nick-Mazuk commented on Apr 29, 2021

@Nick-Mazuk
stalkerg

stalkerg commented on May 1, 2021

@stalkerg
parischap

parischap commented on May 4, 2021

@parischap

70 remaining items

rChaoz

rChaoz commented on May 7, 2024

@rChaoz
Contributor

I think they should, but I'm not sure how. Without anything special needed, a layout can do something like this:

// +layout.svelte

<script>
    const slots = $state({
        header: null,
        footer: null,
    })
    setContext("layoutSlots", slots)
</script>

<div class="header">
    {@render slots.header}
</div>

<main>
    <slot/>
<main>

<div class="footer">
    {@render slots.footer}
</div>

And:

// +page.svelte

<script>
    const slots = getContext("layoutSlots")
    slots.header = header
    onDestroy(() => {
        slots.header = null
    })
</script>

{#snippet header()}
    ...
{/snippet}

This works, but I would love to see a better mechanism for this, that ensures all slots are loaded/unloaded at the same time, as they normally would in a component. One idea would be that top-level snippets in a page are passed as slots, but I don't think would be too great.

Additionally, since slots are now just snippets (plain JS values) passed as props, it would be nice to be able to easily pass any data from the page to the layout. One idea might be a designated export const value is passed as spread props to the layout. Then, this would be very easy to do:

// +page.svelte
import type { LayoutProps } from "./$types"

export const layoutProps: LayoutProps = {
    header,  // snippet
    other: 5,  // arbitrary value
}

Could even be made reactive with export const x = $state({ ...}).
Proof of concept

Bishwas-py

Bishwas-py commented on Jun 18, 2024

@Bishwas-py
Contributor

#627 (comment) Is this solution implemented on the kit code base?

emmnull

emmnull commented on Jun 20, 2024

@emmnull

#627 (comment) Passing snippets from children pages/layouts to parent layouts using a context like this works well in simple cases, but it quickly becomes trickier if you want to properly handle reverting to parent snippets when unmounting nested routes/layouts.

For example, in a dashboard layout where children routes can set different sidebar / header / footer content, the context could look more like:

<!-- /(dashboard)/+layout.svelte -->
<script lang="ts">
  let header = $state<Snippet[]>([]);
  let sidebar = $state<Snippet[]>([]);
  let footer = $state<Snippet[]>([]);

  setContext('dashboard', {
    setHeader(snippet: Snippet) {
      header.push(snippet);
      return onDestroy(() => {
        header.pop();
      });
    },
    setSidebar(snippet: Snippet) {
      sidebar.push(snippet);
      return onDestroy(() => {
        sidebar.pop();
      });
    },
    setFooter(snippet: Snippet) {
      footer.push(snippet);
      return onDestroy(() => {
        footer.pop();
      });
    },
  });

  let { children }: { children: Snippet } = $props();
</script>

<div>
  {#if header.length}
    <header>
      {@render header[header.length - 1]?.()}
    </header>
  {/if}
  {#if sidebar.length}
    <nav>
      {@render sidebar[sidebar.length - 1]?.()}
    </nav>
  {/if}
  <article>
    {@render children()}
  </article>
  {#if footer.length}
    <footer>
      {@render footer[header.length - 1]?.()}
    </footer>
  {/if}
</div>

This context can then be used simply like so:

<!-- /(dashboard)/projects/+page.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';

  let { data } = $props();
  const { setHeader, setSidebar } = getContext('dashboard');

  setHeader(header);
  setSidebar(sidebar);
</script>

{#snippet header()}
  <div>Some header</div>
{/snippet}

{#snippet sidebar()}
  <section>Some links</section>
{/snippet}

<h1>{data.project.title}</h1>
cmolina

cmolina commented on Jul 23, 2024

@cmolina

Based on #627 (comment), I implemented a wrapper for the Context API that allows you to get & set only the last element of a property, and supports dynamic properties.

<!-- /(dashboard)/+layout.svelte -->
<script lang="ts">
  import { setStackContext } from '$lib/stackContext.svelte.js';

  const dashboard = setStackContext('dashboard', {});

  let { children }: { children: Snippet } = $props();
</script>

<div>
  <header>
    {@render dashboard.header?.()}
  </header>
  <nav>
    {@render dashboard.sidebar?.()}
  </nav>
  <article>
    {@render children()}
  </article>
  <footer>
    {@render dashboard.footer?.()}
  </footer>
</div>
<!-- /(dashboard)/projects/+page.svelte -->
<script lang="ts">
  import { getStackContext } from '$lib/stackContext.svelte.js';

  let { data } = $props();
  const dashboard = getStackContext('dashboard');
  dashboard.header = header;
  dashboard.sidebar = sidebar;
</script>

{#snippet header()}
  <div>Some header</div>
{/snippet}

{#snippet sidebar()}
  <section>Some links</section>
{/snippet}

<h1>{data.project.title}</h1>

Internally, it works by keeping an internal object with stacks, and by returning a Proxy for end user consumption.

// src/lib/stackContext.svelte.ts
import { getContext, onDestroy, setContext } from 'svelte';

type RecordStack<V> = { [P in string]: Array<V> }

export function setStackContext<V>(key: string, context: Record<string, V>) {
  const internalRecordStack = $state(createRecordStackFrom<V>(context));

  const proxy = setContext(Symbol.for(key), createPublicProxyFor<V>(internalRecordStack))

  return proxy;
}

function createRecordStackFrom<V>(context: Record<string, V>) {
  return Object.entries(context).reduce((acc, [k, v]) => {
    acc[k] = [v];
    return acc;
  }, {} as RecordStack<V>);
}

function createPublicProxyFor<V>(internalRecordStack: RecordStack<V>) {
  return new Proxy(internalRecordStack, {
    get(target, propertyKey, receiver) {
      if (typeof propertyKey === 'symbol') return Reflect.get(target, propertyKey, receiver);

      return Reflect.get(internalRecordStack, propertyKey, receiver)?.at(-1);
    },
    set(target, propertyKey, value, receiver) {
      if (typeof propertyKey === 'symbol') return Reflect.set(target, propertyKey, value, receiver);

      if (!(propertyKey in target)) {
        Reflect.set(target, propertyKey, [], receiver);
      }
      const stack = Reflect.get(target, propertyKey, receiver);
      stack.push(value);

      onDestroy(() => stack.pop());

      return true;
    },
  }) as Record<string, V>;
}

export function getStackContext<V>(key: string) {
  return getContext<ReturnType<typeof setStackContext<V>>>(Symbol.for(key))
}
seo-rii

seo-rii commented on Aug 31, 2024

@seo-rii
Contributor

sveltejs/svelte#12713 (comment)

If svelte supports exporting snippets in the future, I think it can solve this problem with SSR in a very elegant way!

added a commit that references this issue on Jan 24, 2025
Beyondo

Beyondo commented on Jul 26, 2025

@Beyondo

I found a clean solution that does not require changing your exisiting code structure at all. It is majorily inspired by #12928 (comment) but it had a bug that it does not reset the slots, making pages leak their own slots while navigating if they don't override it. I fixed it by simply deleting the slots on every call.

$lib/state/layout.svelte.ts (or anywhere you want):

import { getContext, setContext } from 'svelte';

const key = Symbol('layout-slots');

export function initLayout() {
    const slots = $state({});
    return setContext(key, slots);
}

export function layout(slots) {
    const context = getContext(key);
    Object.keys(context).forEach(key => delete context[key]);
    Object.assign(context, slots);
}

How to use:

+layout.svelte:

<script>
    import { initLayout } from "$lib/state/layout.svelte.js";
    let { children } = $props();
    const slots = initLayout();
</script>
<header>
    {#if !slots.custom}
        <a href="/page2">Default Button</a> <!-- Your Default Slot -->
    {:else}
        {@render slots.custom()}
    {/if}
</header>
<main>
    {@render children()}
</main>

+page.svelte:

<script lang="ts">
    import { layout } from "$lib/state/layout.svelte";
    layout({})
</script>
<div style="pointer-events: none; user-select: none;">
    <h2>Hello from Page 1 & Default header action!</h2>
</div>

page2/+page.svelte:

<script lang="ts">
    import { onMount } from 'svelte';
    import { layout } from "$lib/state/layout.svelte";
    layout({ custom });
</script>
{#snippet custom()}
    <button class="custom-layout-button" onclick={() => alert(`Page-specific layout button!`)}>
        Custom Action
    </button>
{/snippet}
<h2>Hello from Page 2 & Custom Header Action!</h2>

Note 1: If you don't call layout on every page, the slots would not reset and cause them to stay/leak even after navigation.
Note 2: The combined amalgamation of non-snippet code is always implicitly passed as children, so no need to wrap it.
Note 3: Even if you set nothing, you must always call layout({}) in your +page.svelte(s) to clear the slots and thus reset them to whatever default you have in +layout.svelte.

For your mental model: It's just a single extra function call on every page to pass your custom snippets for your layout and your +layout.svelte simply determines where they are placed (wherever you want). This assumes a single layout for the entire application, but you could always extend by copying the layout.svelte.ts file as much you want and rename it or structure it better for your own project to add more reactive state tracking.

t1u1

t1u1 commented on Jul 30, 2025

@t1u1

The solution I ended up with is to ditch +layout.svelte completely. Instead, I define my own PageLayout components and use them manually in each page route. That way, I have complete control over the layout, including parameters and slots. I still use the +layout.server file for auth and other cross cutting concerns. In a pinch, for a particular subroute, I have the option to drop a +layout.svelte and include the PageLayout component in it.

Have used this for multiple projects now without any regrets.

valentinschabschneider

valentinschabschneider commented on Jul 31, 2025

@valentinschabschneider

We are encountering layouts where this would be a life savior! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature / enhancementNew feature or requestsvelteThis feature can't be implemented without changes to Svelte itself

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @brandonbuttars@Bandit@joakim@buhrmi@Palmik

        Issue actions

          Treat layout pages like components when using named slots · Issue #627 · sveltejs/kit