Skip to main content
Technology & EngineeringSvelte368 lines

Component Patterns

Svelte component composition patterns including props, snippets, context, and advanced reuse techniques

Quick Summary24 lines
You are an expert in Svelte component design, helping developers build reusable, composable components using props, snippets, context, and advanced patterns.

## Key Points

- **Use snippets instead of multiple component files** when the markup is only relevant in one context. Extract to a component when it is reused across different parents.
- **Type your props** with TypeScript or JSDoc for IDE support and compile-time safety.
- **Use context sparingly.** It makes data flow implicit. Prefer props for direct parent-child communication and reserve context for deep or cross-cutting concerns (themes, i18n, auth).
- **Keep components focused.** A component should have a single responsibility. Use composition (snippets, context, actions) to add behavior rather than bloating a single component.
- **Use `$bindable()`** only when two-way binding genuinely simplifies the API (form inputs, toggles). For most props, one-way data flow with callbacks is clearer.
- **Destructuring `$props()` at the wrong level.** `$props()` must be called at the top level of `<script>`. You cannot call it inside a function or conditional.
- **Forgetting `{@render children()}`** in a component that wraps content. Without it, child content silently disappears.
- **Stale context values.** `getContext` returns the value at the time the child component initializes. Use a getter pattern (as shown above) or a store/rune to keep it reactive.
- **Overusing `svelte:component`.** Dynamic components have overhead. If you have a small, fixed set of variants, an `{#if}` chain is simpler and more tree-shakeable.

## Quick Example

```svelte
<!-- Usage -->
<Button variant="danger" onclick={handleDelete}>
  Delete Item
</Button>
```
skilldb get svelte-skills/Component PatternsFull skill: 368 lines
Paste into your CLAUDE.md or agent config

Component Composition Patterns — Svelte

You are an expert in Svelte component design, helping developers build reusable, composable components using props, snippets, context, and advanced patterns.

Core Philosophy

Svelte's component model is built on the principle that the compiler should do the heavy lifting, not the developer. Where other frameworks require explicit reactivity wrappers, virtual DOM diffing, or runtime dependency tracking, Svelte compiles components into efficient imperative code at build time. This means component patterns in Svelte should lean into the compiler's strengths — using $props(), snippets, and context as the framework intends rather than fighting the model with patterns borrowed from React or Vue.

Composition in Svelte follows a "progressive disclosure" approach. Simple components use props and children content. When you need parameterized child content, you reach for snippets. When you need to share state across a subtree without prop drilling, you use context. When you need reusable DOM behavior, you use actions. Each mechanism is a step up in power and complexity, and the right choice depends on how deep the composition needs to go. The mistake is reaching for context or stores when props would suffice, or using multiple component files when a single snippet would be clearer.

Svelte 5's shift to $props() and snippets over slots and $$props is not cosmetic — it brings type safety, explicit data flow, and better tooling support. Snippets are typed, can receive parameters, and compose naturally. The {@render} tag makes it clear where child content appears in the markup. Designing components around these primitives produces APIs that are self-documenting and hard to misuse.

Anti-Patterns

  • Overusing Context for Direct Parent-Child Communication — reaching for setContext/getContext when simple props would suffice. Context makes data flow implicit, which is justified for deep hierarchies but unnecessary overhead for one-level-deep communication.

  • Forgetting {@render children()} — creating wrapper components that accept child content but never call {@render children()} in their template, causing the content to silently disappear with no error.

  • Stale Context Without Reactive Getters — providing a plain value via setContext that becomes stale because getContext captures the value at initialization time. Use a getter pattern or a store/rune to keep context values reactive.

  • Dynamic Component Overuse — using <svelte:component this={...}> for a small, fixed set of variants when an {#if} chain would be simpler, more tree-shakeable, and easier for the compiler to optimize.

  • Calling $props() Inside Functions or Conditionals — attempting to call $props() anywhere other than the top level of <script>, which fails because the compiler requires it to be a top-level destructuring declaration.

Overview

Svelte 5 introduces a refined component model centered on $props(), snippets ({#snippet}), and the {@render} tag. These replace the older slot and $$props APIs. Combined with the context API and reactive primitives, they enable expressive, type-safe component composition.

Core Concepts

Props with $props()

Components declare their props via destructuring $props():

<!-- Button.svelte -->
<script>
  let { variant = 'primary', disabled = false, onclick, children } = $props();
</script>

<button class="btn btn-{variant}" {disabled} {onclick}>
  {@render children()}
</button>
<!-- Usage -->
<Button variant="danger" onclick={handleDelete}>
  Delete Item
</Button>

Rest Props and Spreading

Forward unknown props to a native element:

<script>
  let { class: className = '', children, ...rest } = $props();
</script>

<div class="card {className}" {...rest}>
  {@render children()}
</div>

Snippets (Replacing Slots)

Snippets are typed, reusable chunks of markup. They replace Svelte 4's <slot> system.

<!-- List.svelte -->
<script>
  let { items, row } = $props();
</script>

<ul>
  {#each items as item}
    <li>{@render row(item)}</li>
  {/each}
</ul>
<!-- Usage -->
<List items={users}>
  {#snippet row(user)}
    <span>{user.name}</span> — <span>{user.email}</span>
  {/snippet}
</List>

Default Snippet Content

Provide fallback content when a snippet is not supplied:

<script>
  let { header, children } = $props();
</script>

<div class="panel">
  <div class="panel-header">
    {#if header}
      {@render header()}
    {:else}
      <h3>Default Title</h3>
    {/if}
  </div>
  <div class="panel-body">
    {@render children()}
  </div>
</div>

Context API

Share data across a component subtree without prop drilling:

<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from 'svelte';
  let { theme = 'light', children } = $props();
  setContext('theme', { get current() { return theme; } });
</script>

{@render children()}
<!-- DeepChild.svelte -->
<script>
  import { getContext } from 'svelte';
  const theme = getContext('theme');
</script>

<div class="card card-{theme.current}">...</div>

Implementation Patterns

Compound Components

Build related components that share state through context:

<!-- Tabs.svelte -->
<script>
  import { setContext } from 'svelte';

  let { children } = $props();
  let activeTab = $state(0);

  setContext('tabs', {
    get activeTab() { return activeTab; },
    setActive(index) { activeTab = index; }
  });
</script>

<div class="tabs">
  {@render children()}
</div>
<!-- TabList.svelte -->
<script>
  let { children } = $props();
</script>

<div role="tablist">
  {@render children()}
</div>
<!-- Tab.svelte -->
<script>
  import { getContext } from 'svelte';
  let { index, children } = $props();
  const tabs = getContext('tabs');
</script>

<button
  role="tab"
  aria-selected={tabs.activeTab === index}
  onclick={() => tabs.setActive(index)}
>
  {@render children()}
</button>
<!-- TabPanel.svelte -->
<script>
  import { getContext } from 'svelte';
  let { index, children } = $props();
  const tabs = getContext('tabs');
</script>

{#if tabs.activeTab === index}
  <div role="tabpanel">
    {@render children()}
  </div>
{/if}
<!-- Usage -->
<Tabs>
  <TabList>
    <Tab index={0}>Overview</Tab>
    <Tab index={1}>Details</Tab>
  </TabList>
  <TabPanel index={0}>Overview content</TabPanel>
  <TabPanel index={1}>Details content</TabPanel>
</Tabs>

Wrapper / Headless Component Pattern

Provide behavior without prescribing markup:

<!-- Hoverable.svelte -->
<script>
  let { children } = $props();
  let hovered = $state(false);
</script>

<div
  onpointerenter={() => hovered = true}
  onpointerleave={() => hovered = false}
>
  {@render children(hovered)}
</div>
<!-- Usage -->
<Hoverable>
  {#snippet children(hovered)}
    <div class:highlighted={hovered}>
      {hovered ? 'Hovering!' : 'Hover me'}
    </div>
  {/snippet}
</Hoverable>

Component Polymorphism with this

Dynamically render different components:

<script>
  import Alert from './Alert.svelte';
  import Info from './Info.svelte';
  import Warning from './Warning.svelte';

  const components = { alert: Alert, info: Info, warning: Warning };

  let { type = 'info', children } = $props();
</script>

<svelte:component this={components[type]}>
  {@render children()}
</svelte:component>

Action-Based Composition

Encapsulate reusable DOM behavior with actions:

// clickOutside.js
export function clickOutside(node, callback) {
  function handleClick(event) {
    if (!node.contains(event.target)) {
      callback();
    }
  }

  document.addEventListener('click', handleClick, true);

  return {
    destroy() {
      document.removeEventListener('click', handleClick, true);
    }
  };
}
<script>
  import { clickOutside } from './clickOutside.js';
  let open = $state(false);
</script>

{#if open}
  <div class="dropdown" use:clickOutside={() => open = false}>
    Dropdown content
  </div>
{/if}

Forwarding Events

In Svelte 5, events are just callback props:

<!-- SearchInput.svelte -->
<script>
  let { value = $bindable(''), onsubmit, ...rest } = $props();
</script>

<form onsubmit|preventDefault={onsubmit}>
  <input bind:value {...rest} />
</form>

Bindable Props

Use $bindable() for two-way binding support:

<!-- Toggle.svelte -->
<script>
  let { checked = $bindable(false), label } = $props();
</script>

<label>
  <input type="checkbox" bind:checked />
  {label}
</label>
<!-- Usage -->
<script>
  let agreed = $state(false);
</script>

<Toggle bind:checked={agreed} label="I agree" />
<p>Agreed: {agreed}</p>

Best Practices

  • Use snippets instead of multiple component files when the markup is only relevant in one context. Extract to a component when it is reused across different parents.
  • Type your props with TypeScript or JSDoc for IDE support and compile-time safety.
  • Use context sparingly. It makes data flow implicit. Prefer props for direct parent-child communication and reserve context for deep or cross-cutting concerns (themes, i18n, auth).
  • Keep components focused. A component should have a single responsibility. Use composition (snippets, context, actions) to add behavior rather than bloating a single component.
  • Use $bindable() only when two-way binding genuinely simplifies the API (form inputs, toggles). For most props, one-way data flow with callbacks is clearer.

Common Pitfalls

  • Destructuring $props() at the wrong level. $props() must be called at the top level of <script>. You cannot call it inside a function or conditional.
  • Forgetting {@render children()} in a component that wraps content. Without it, child content silently disappears.
  • Stale context values. getContext returns the value at the time the child component initializes. Use a getter pattern (as shown above) or a store/rune to keep it reactive.
  • Overusing svelte:component. Dynamic components have overhead. If you have a small, fixed set of variants, an {#if} chain is simpler and more tree-shakeable.
  • Passing snippets as regular props vs. child content. Named snippets defined inside a component's tag become props. The implicit children snippet is the unnamed content between the opening and closing tags.

Install this skill directly: skilldb add svelte-skills

Get CLI access →