Four Patterns for Data Flow

Components talk to each other. Parents pass data down. Children notify parents of events. Sometimes data flows both ways.

Svelte 5 gives you clear patterns for each:

  • $props — Data flowing down
  • Callbacks — Events flowing up
  • $bindable — Two-way binding (use sparingly)
  • Snippets — Flexible content composition

Master these four, and you can build any component API.

Props: Data Down

Props are how parents pass data to children. Declare them with $props():

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

<button class="btn btn-{variant}" {disabled}>
  {label}
</button>

Usage:

<Button label="Save" />
<Button label="Cancel" variant="secondary" />
<Button label="Delete" variant="danger" disabled={isDeleting} />

Props are reactive. When the parent’s value changes, the child updates automatically.

Rest props

Capture remaining props with rest syntax:

<!-- Input.svelte -->
<script>
  let { label, error, ...rest } = $props();
</script>

<label>
  {label}
  <input {...rest} class:error={!!error} />
  {#if error}
    <span class="error-message">{error}</span>
  {/if}
</label>

Now any HTML input attribute works:

<Input label="Email" type="email" required placeholder="you@example.com" />
<Input label="Age" type="number" min={0} max={120} />

TypeScript

Type your props for better DX:

<script lang="ts">
  interface Props {
    label: string;
    variant?: 'primary' | 'secondary' | 'danger';
    disabled?: boolean;
    onclick?: () => void;
  }
  
  let { label, variant = 'primary', disabled = false, onclick }: Props = $props();
</script>

The compiler and your editor now catch mistakes.

Callbacks: Events Up

When children need to notify parents, use callback props:

<!-- SearchInput.svelte -->
<script>
  let { value = '', onchange, onsearch } = $props();
  
  function handleKeydown(event) {
    if (event.key === 'Enter') {
      onsearch?.(value);
    }
  }
</script>

<input 
  type="search"
  {value}
  oninput={(e) => onchange?.(e.target.value)}
  onkeydown={handleKeydown}
/>

Usage:

<script>
  let query = $state('');
  
  function handleSearch(value) {
    console.log('Searching for:', value);
  }
</script>

<SearchInput 
  value={query} 
  onchange={(v) => query = v}
  onsearch={handleSearch}
/>

The child calls the callback. The parent decides what to do. Data flows up through function calls.

Convention: on* prefix

Name callback props with on prefix: onclick, onchange, onsubmit, onselect. This matches HTML event naming and makes the prop’s purpose clear.

<script>
  let { 
    onselect,      // Called when user selects an item
    ondelete,      // Called when user deletes an item  
    onreorder      // Called when user reorders items
  } = $props();
</script>

Bindable: Two-Way Binding

Sometimes props need to flow both directions. A form input’s value changes from both parent and child. Use $bindable():

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

<label>
  {label}
  <input bind:value />
</label>

The parent can now use bind::

<script>
  let name = $state('');
</script>

<TextInput label="Name" bind:value={name} />
<p>Hello, {name}</p>

Changes flow both ways:

  • Parent changes name → input updates
  • User types in input → name updates

When to use $bindable

Two-way binding is powerful but creates implicit coupling. Use it for:

  • Form inputs (text, checkbox, select)
  • Controlled components that wrap native inputs
  • Modal/dialog open state

Don’t use it for:

  • Action callbacks (use onclick, onsubmit)
  • Complex state (use explicit callbacks)
  • Anything where the child shouldn’t modify the value

When in doubt, use callbacks. They’re more explicit:

<!-- Explicit: parent controls the value -->
<TextInput 
  value={name} 
  onchange={(v) => name = v} 
/>

<!-- Implicit: either side can change it -->
<TextInput bind:value={name} />

Both work. The explicit version makes data flow clearer.

Snippets: Flexible Content

Snippets replace slots from Svelte 4. They’re more powerful and more explicit.

Basic snippets

<!-- Card.svelte -->
<script>
  let { header, children } = $props();
</script>

<div class="card">
  {#if header}
    <div class="card-header">
      {@render header()}
    </div>
  {/if}
  
  <div class="card-body">
    {@render children()}
  </div>
</div>

Usage:

<Card>
  {#snippet header()}
    <h2>Card Title</h2>
  {/snippet}
  
  <p>This is the card content.</p>
</Card>

Content between the component tags (without a {#snippet} wrapper) becomes the children prop.

Snippets with parameters

Snippets can receive data from the component:

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

<ul>
  {#each items as item, index}
    <li>
      {@render renderItem(item, index)}
    </li>
  {/each}
</ul>

Usage:

<script>
  let users = $state([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]);
</script>

<List items={users}>
  {#snippet renderItem(user, index)}
    <span>{index + 1}. {user.name}</span>
  {/snippet}
</List>

The component controls when to render. The parent controls what to render. This is the “render prop” pattern, made clean.

Real example: Modal

<!-- Modal.svelte -->
<script>
  let { open = $bindable(false), title, children, footer } = $props();
  
  function close() {
    open = false;
  }
</script>

{#if open}
  <div class="modal-backdrop" onclick={close}>
    <div class="modal" onclick={(e) => e.stopPropagation()}>
      <header class="modal-header">
        <h2>{title}</h2>
        <button class="close" onclick={close}>×</button>
      </header>
      
      <div class="modal-body">
        {@render children()}
      </div>
      
      {#if footer}
        <footer class="modal-footer">
          {@render footer()}
        </footer>
      {/if}
    </div>
  </div>
{/if}

Usage:

<script>
  let showConfirm = $state(false);
  
  function handleDelete() {
    // ... delete logic
    showConfirm = false;
  }
</script>

<button onclick={() => showConfirm = true}>Delete</button>

<Modal bind:open={showConfirm} title="Confirm Delete">
  <p>Are you sure you want to delete this item?</p>
  
  {#snippet footer()}
    <button onclick={() => showConfirm = false}>Cancel</button>
    <button class="danger" onclick={handleDelete}>Delete</button>
  {/snippet}
</Modal>

The modal handles:

  • Open/close state (via $bindable)
  • Layout and styling
  • Backdrop click to close

The parent handles:

  • When to show it
  • What content to display
  • What actions to offer

Clean separation.

Putting It Together: DataTable

A real component often uses all these patterns:

<!-- DataTable.svelte -->
<script>
  let { 
    items,
    columns,
    renderCell,
    onrowclick,
    selectedId = $bindable(null)
  } = $props();
</script>

<table>
  <thead>
    <tr>
      {#each columns as column}
        <th>{column.label}</th>
      {/each}
    </tr>
  </thead>
  <tbody>
    {#each items as item (item.id)}
      <tr 
        class:selected={item.id === selectedId}
        onclick={() => {
          selectedId = item.id;
          onrowclick?.(item);
        }}
      >
        {#each columns as column}
          <td>
            {#if renderCell}
              {@render renderCell(item, column)}
            {:else}
              {item[column.key]}
            {/if}
          </td>
        {/each}
      </tr>
    {/each}
  </tbody>
</table>

Usage:

<script>
  let users = $state([/* ... */]);
  let selectedUserId = $state(null);
  
  const columns = [
    { key: 'name', label: 'Name' },
    { key: 'email', label: 'Email' },
    { key: 'role', label: 'Role' }
  ];
  
  function handleRowClick(user) {
    console.log('Selected:', user);
  }
</script>

<DataTable 
  items={users}
  {columns}
  bind:selectedId={selectedUserId}
  onrowclick={handleRowClick}
>
  {#snippet renderCell(user, column)}
    {#if column.key === 'role'}
      <span class="badge badge-{user.role}">{user.role}</span>
    {:else}
      {user[column.key]}
    {/if}
  {/snippet}
</DataTable>

This table uses:

  • Props: items, columns (data down)
  • Callbacks: onrowclick (events up)
  • Bindable: selectedId (two-way for controlled selection)
  • Snippets: renderCell (flexible rendering)

The Hierarchy

When designing component APIs, think in this order:

  1. Props first. Most data flows down.
  2. Callbacks for events. When children need to notify parents.
  3. Bindable for forms. When the same value is edited by both sides.
  4. Snippets for content. When parents need to customize what’s rendered.

Keep it simple. A component with 3 props is easier to use than one with 10. Start minimal, add props when you need them.


Next up: We’ve covered how to build. Now let’s talk about maintaining what you’ve built. Starting with the question everyone asks too early: “Should I use a global store?”