Recursive Snippets

+15 Mana ✨

One of the most powerful snippet features is recursion — a snippet can render itself!

Basic Recursion: Tree Structure

svelte
<script>
  let fileTree = $state({
    name: 'src',
    children: [
      { name: 'app', children: [
        { name: 'page.svelte' },
        { name: 'layout.svelte' }
      ]},
      { name: 'lib', children: [
        { name: 'utils.js' },
        { name: 'components', children: [
          { name: 'Button.svelte' },
          { name: 'Card.svelte' }
        ]}
      ]}
    ]
  });
</script>

{#snippet treeNode(node, depth = 0)}
  <div style="padding-left: {depth * 20}px">
    <span class="name">
      {node.children ? '📁' : '📄'} {node.name}
    </span>
    
    {#if node.children}
      {#each node.children as child}
        {@render treeNode(child, depth + 1)}  <!-- Recursion! -->
      {/each}
    {/if}
  </div>
{/snippet}

<div class="file-tree">
  {@render treeNode(fileTree)}
</div>

Expandable Tree

svelte
<script>
  let expanded = $state(new Set());
  
  function toggle(path) {
    if (expanded.has(path)) {
      expanded.delete(path);
    } else {
      expanded.add(path);
    }
    expanded = expanded;  // Trigger update
  }
</script>

{#snippet node(item, path = '')}
  {@const currentPath = `${path}/${item.name}`}
  {@const isExpanded = expanded.has(currentPath)}
  {@const hasChildren = item.children?.length > 0}
  
  <div class="node">
    <button 
      class="toggle"
      onclick={() => hasChildren && toggle(currentPath)}
      disabled={!hasChildren}
    >
      {#if hasChildren}
        {isExpanded ? '▼' : '▶'}
      {:else}
        •
      {/if}
    </button>
    <span class="label">{item.name}</span>
  </div>
  
  {#if hasChildren && isExpanded}
    <div class="children">
      {#each item.children as child}
        {@render node(child, currentPath)}
      {/each}
    </div>
  {/if}
{/snippet}

{@render node(data)}

Comment Thread Example

svelte
{#snippet comment(c, depth = 0)}
  <article class="comment" style="--depth: {depth}">
    <header>
      <img src={c.author.avatar} alt="" />
      <strong>{c.author.name}</strong>
      <time>{c.date}</time>
    </header>
    <p>{c.text}</p>
    <button onclick={() => reply(c.id)}>Reply</button>
    
    {#if c.replies?.length}
      <div class="replies">
        {#each c.replies as reply}
          {@render comment(reply, depth + 1)}  <!-- Nested! -->
        {/each}
      </div>
    {/if}
  </article>
{/snippet}

{#each comments as c}
  {@render comment(c)}
{/each}

Base Case is Essential

Like any recursion, you need a termination condition:

svelte
{#snippet countDown(n)}
  {#if n <= 0}    <!-- Base case -->
    <p>Done!</p>
  {:else}
    <p>{n}...</p>
    {@render countDown(n - 1)}  <!-- Recursive case -->
  {/if}
{/snippet}
✓ Completed