Svelte Snippets - Say Goodbye To Slots

| Published: February 23, 2024

| Updated: February 23, 2024

Everyone is talking about the upcoming release of Svelte 5 and the major changes brought with it including the new Runes syntax. But there’s also another big change to how you’ll create Svelte components, and that is with the new Snippets feature.

The introduction of snippets brings with it the deprecation of the <slot /> tag, which allowed you to pass markup to a component as children. Svelte developers will now have to re-learn this new paradigm, which massively improves dev-experience; particularly for library creators.

Snippets are a massive upgrade from slots

This article will walk you through:

  • How to use snippets,
  • How you can achieve the same results using snippets as you would have previously using <slot />, and
  • Some examples showing the power of snippets and how they allow us to write more readable code.

Skip to FAQ’s

What is a Svelte Snippet?

Snippets are simply a way to create reusable chunks of markup inside your Svelte components. Snippets can be passed to other components as props or children, similarly to how you would use a <slot /> tag. They even accept parameters to render dynamic content or be used with {#each} blocks.

Learning this new paradigm is best conceptualized by looking at some examples of how write snippets, so in the next section we’ll look at 3 examples snippets:

  1. Basic Usage Example - No parameters,
  2. Slight more advanced example - with parameters,
  3. Advanced Example - recreating a UI component using Snippets

In the last example, we’ll first look at how something is currently achieved using <slot /> (Svelte 4 and earlier), and the compare it to the approach with snippets (Svelte 5).

via GIPHY

Basic Usage

To create a snippet, simply wrap a chunk of markup inside some {#snippet} tags, like this:

{#snippet snippetName()}
    <!-- Snippet Content -->
{/snippet}

And you can render the snippet markup using {@render snippetName()}, for example:

{#snippet snippetName()}
    <p>This is the snippet content</p>
{/snippet}

{@render snippetName()}

And now you can use this snippet anywhere you like, including passing it into other components like this:

<ChildComponent {snippetName} />

Which you can accept as props the child component using the $props() rune, like this:

ChildComponent.svelte

<script>
    let { snippetName } = $props();
</script>

<div>
    {@render snippetName()}
</div>

Nice one! This is how you pass HTML chunks into other components using snippets.

via GIPHY

But this example isn’t entirely useful, so let’s look at a more realistic example where we can pass arguments to the snippet, and use it inside an {#each} block.

Using Snippets with parameters

To create a snippet with parameters, you just add arguments into the snippet declaration:

{#snippet userPreview(user, buttonText, buttonAction)}
    <div>
        <h2>{user.name}</h2>
        <img src={user.profileImage} alt="Profile Image" />
        <button onClick={() => buttonAction(user.id)}>{buttonText}</button>
    </div>
{/snippet}

Now let’s pass the snippet and the relevant data into 2 child components:

  • <FriendsList /> and
  • <SuggestedFriendsList />

+page.svelte

<script lang='ts>
	let friendsData: {id: string; name: string; profileImage: string}[] = []
	let suggestedFriendsData: {id: string; name: string; profileImage: string}[] = []
</script>

{#snippet userPreview(user, buttonText, buttonAction)}
    <div>
        <h2>{user.name}</h2>
        <img src={user.profileImage} alt="Profile Image" />
        <button onClick={() => buttonAction(user.id)}>{buttonText}</button>
    </div>
{/snippet}

<FriendsList {friendsData} {userPreview} />
<SuggestedFriendsList {suggestedFriendsData} {userPreview} />

Let’s look at how to use the passed snippet in the <SuggestedFriendsList /> child component:

SuggestedFriendsList.svelte

<script lang="ts">
    import type { Snippet } from 'svelte';
    import type { UserType } from '$lib/types.d.ts'
   
    let { friendsData, userPreview } = $props<{
	    friendsData: {id: string; name: string; profileImage: string}[];
	    userPreview: Snippet<{user: UserType, buttonText: string; buttonAction: (id: string) => void}>
    }>();
   
    async function addUserAsFriend(id) {
	    await db.addFriend(id)
    }
</script>

<h1>Your Friends</h1>

{#each friendsData as friend}
    {@render userPreview(friend, "Add Friend", addUserAsFriend)}
{/each}

Now you have passed the relevant markup into the child component, and you can declare as much or as little data in the snippet as you need.

via GIPHY

The implementation of the <FriendsList /> component is trivial and could resemble something similar to the <SuggestedFriendsList /> component, however instead the buttonAction could delete a friend, and the button text would correspond. We just split them up to illustrate how we can reuse the markup and maintain flexibility.

Now we know how to provide arguments to snippets, let’s look at something a little more advanced.

via GIPHY

Using Svelte Snippets - Advanced Example

In this example, we’ll look at a real life example of porting a React UI component to Svelte. This example is from the Aceternity Svelte library I ported over from Aceternity UI, which was originally created with, and for React. More specifically, we’ll be looking at the Aceternity Tabs component.

Seeing as the Svelte port was made with Svelte 4, I didn’t have access to snippets - so we will highlight how the lack of snippets lead to untidy and duplicated code. Furthermore, we’ll show how with snippets, we can create a more 1-to-1 translation from React components.

In the interest of code cleanliness, we’ll remove tailwind class styles and some functionality from the components.

via GIPHY

In the original React component, the component source code (components/ui/tabs.tsx) is one file:

REACT component source code

'use client';

import { useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '@/utils/cn';

type Tab = {
	title: string;
	value: string;
	content?: string | React.ReactNode | any;
};

export const Tabs = ({
	tabs: propTabs,
	containerClassName,
	activeTabClassName,
	tabClassName,
	contentClassName
}: {
	tabs: Tab[];
	containerClassName?: string;
	activeTabClassName?: string;
	tabClassName?: string;
	contentClassName?: string;
}) => {
	const [hovering, setHovering] = useState(false);

	return (
		<>
			<div>
				{propTabs.map((tab, idx) => (
					<button>
						{active.value === tab.value && <motion.div />}

						<span>{tab.title}</span>
					</button>
				))}
			</div>
			<FadeInDiv tabs={tabs} active={active} key={active.value} hovering={hovering} />
		</>
	);
};

export const FadeInDiv = ({
	className,
	tabs,
	hovering
}: {
	className?: string;
	key?: any;
	tabs: Tab[];
	active: Tab;
	hovering?: boolean;
}) => {
	const isActive = (tab: Tab) => {
		return tab.value === tabs[0].value;
	};
	return (
		<div>
			{tabs.map((tab, idx) => (
				<motion.div>{tab.content}</motion.div>
			))}
		</div>
	);
};

To port this over without snippets, we have to split the 2 JSX components (in this 1 file) into 2 files. We had to move the FadeInDiv to its own .svelte component file, and then import it in the main Tabs.svelte component, like this:

components/Tabs.svelte

<script lang="ts">
	import { Motion } from 'svelte-motion';
	import FadeInDiv from './FadeInDiv.svelte';

	type Tab = {
		title: string;
		value: string;
		content?: string | any;
	};

	export let propTabs: Tab[];
	export let containerClassName: string | undefined = undefined;
	export let activeTabClassName: string | undefined = undefined;
	export let tabClassName: string | undefined = undefined;
	export let contentClassName: string | undefined = undefined;

	let active: Tab = propTabs[0];
	let tabs: Tab[] = propTabs;
	let hovering = false;
</script>

<div>
	{#each propTabs as tab, idx (tab.title)}
		<button>
			{#if active.value === tab.value}
				<Motion>
					<div />
				</Motion>
			{/if}
			<span>
				{tab.title}
			</span>
		</button>
	{/each}
</div>

<FadeInDiv {tabs} {hovering} />

So, without snippets, we’ve had to move our code into 2 separate files - even if the <FadeInDiv /> component would only be used in this <Tabs /> component.

Now, with snippets, we could just convert the <FadeInDiv /> markup into a snippet:

{#snippet fadeInDiv(className, tabs, hovering)}
    <div>
        {#each tabs as tab, idx}
            <motion.div>
                {tab.content}
            </motion.div>
        {/each}
    </div>
{/snippet}

and then call it from the same file:

components/Tabs.svelte

<script lang='ts'>
	// All the script content
</script>

<!-- All the additional tabs content -->

{@render fadeInDiv('mt-32', tabs, hovering)}

{#snippet fadeInDiv(className, tabs, hovering)}
    <div>
        {#each tabs as tab, idx}
            <motion.div>
                {tab.content}
            </motion.div>
        {/each}
    </div>
{/snippet}

So long as the snippet is within the same lexical scope as the markup using it, we’ve now been able to move the 2 files (Tabs.svelte and FadeInDiv.svelte) into the 1 file. 🎉

via GIPHY

So far, we’ve only looked at the source code of the component. Now let’s look at the how you would use this component in one of your pages.

In the original React usage code, you just have to declare the tabs array and then call <Tabs tabs={tabs} />, like this:

REACT Usage of Tabs component

export function TabsDemo() {
	const tabs = [
		{
			title: 'Product',
			value: 'product',
			content: (
				<div>
					<p>Product Tab</p>
					<DummyContent imageSrc="https://placehold.co/800x600" />
				</div>
			)
		},
		{
			title: 'Services',
			value: 'services',
			content: (
				<div>
					<p>Services tab</p>
					<DummyContent imageSrc="https://placehold.co/600x400" />
				</div>
			)
		}
	];

	return <Tabs tabs={tabs} />;
}

const DummyContent = (imageSrc) => {
	return <Image src={imageSrc} />;
};

There’s quite a bit going on here which Svelte 4 can’t handle simply.

Svelte 4 just isn't good enough.

Firstly, we need to deal with the DummyContent component at the bottom of the file. For this, we have 2 options:

  1. Create a new DummyContent.svelte component, and import it in our page, or
  2. Copy the markup of the DummyContent wherever it is used, without care for duplicate code.

With the example above, it seems obvious that you should just replace the <DummyContent /> declarations with the actual markup of the component. However, in this example a lot of the functionality has been stripped, and as it gets more verbose (or the list gets larger), it’s better to separate it off as its own component. You don’t want massive blobs of duplicative code.

Secondly, with Svelte you can’t use markup as a property value in your objects, as it is being done in the content property for each Tab.

So in my port, I converted the content in each Tab to a string. Then using the special {@html} tag, I was able to render the string as HTML in the Tabs.svelte component, mentioned earlier:

<script lang="ts">
	import Tabs from './Tabs.svelte';

	const tabs = [
		{
			title: 'Product',
			value: 'product',
			content: '<div> <p>Product Tab</p> <img src="/linear.webp" /> </div>'
		},
		{
			title: 'Services',
			value: 'services',
			content: '<div> <p>Services tab</p> <img src="/linear.webp" /> </div>'
		}
	];
</script>

<Tabs propTabs={tabs} contentClassName="rounded-2xl" />

I also moved the DummyContent markup into the content value. 🤦 As I mentioned, it’s definitely not clean - but with Svelte 4, it’s likely the cleanest way to get around this.

via GIPHY

Writing stringified HTML as a prop is unpleasant and in most cases should be avoided. It’s prone to errors, you don’t get syntax highlighting, and could potentially open users up to XSS vulnerabilities if the data comes from an untrusted source and isn’t sanitized.

So now with Svelte 5 snippets, we could convert the DummyContent component, and the content props to snippets:

{#snippet dummyContent(imageSrc)}
    <img src={imageSrc} />
{/snippet}

{#snippet content(title, imageSrc)}
    <div>
        <p>{title} Tab</p>
        {@render dummyContent(imageSrc)}
    </div>
{/snippet}

And then we can get rid of the content property in our tabs array, and pass the content snippet to the <Tabs /> component:

<script lang="ts">
    import Tabs from './Tabs.svelte';

    const tabs = [
        {
            title: 'Product',
            value: 'product',
            imageSrc: 'https://placehold.co/800x600',
        },
        {
            title: 'Services',
            value: 'services',
            imgSrc: 'https://placehold.co/600x400',
        },
    ];
</script>

{#snippet dummyContent(imageSrc)}
    <img src={imageSrc} />
{/snippet}

{#snippet content(title, imageSrc)}
    <div>
        <p>{title} Tab</p>
        {@render dummyContent(imageSrc)}
    </div>
{/snippet}

<Tabs
    propTabs={tabs}
    contentClassName="rounded-2xl"
    {content}
/>

There are a few benefits here:

  1. We don’t have to create a separate component file for DummyContent, and
  2. We don’t have to use stringified HTML in our tabs objects.

And then we can adjust the Tabs.svelte file to accept the snippet like this:

<script lang='ts'>
	import type { Snippet } from 'svelte';

	let {propTabs, contentClassName, content} = $props<{
		propTabs: Tab;
		contentClassName: string;
		content: Snippet<{title: string; imageSrc: string;}[]>;
	}>
</script>

<!-- All the additional tabs content -->

{@render fadeInDiv('mt-32', tabs, hovering)}

{#snippet fadeInDiv(className, tabs, hovering)}
    <div>
        {#each propTabs as tab, idx}
            <motion.div>
                {@render content(tab.title, tab.imageSrc)}
            </motion.div>
        {/each}
    </div>
{/snippet}

And now we’ve moved the old Svelte 4 way of porting over this component with slots and chaos, to now using snippets.

via GIPHY

In Summary

Snippets are a much more powerful and flexible upgrade from <slot /> feature from Svelte 4 and earlier. As we’ve seen, the benefits snippets bring are:

  1. being able to effectively use as many ‘components’ as we want in 1 .svelte component file,
  2. pass in HTML blocks as arguments to child components without having to stringify the HTML value,
  3. handle duplicate code beautifully,
  4. Add markup as children - similarly to the <slot /> tag.

It’s an awesome feature which has been overshadowed with the monolithic change brought by runes, however Snippets will make building apps - and moreso building libraries, a much better experience with Svelte.

Remember, Svelte 5 isn’t quite ready yet, so you can play with Snippets in the Svelte 5 preview playground, or create a project using the Svelte 5 preview.

FAQ’s

Thanks for reading ❤️

Here are some other articles I think you might like!