Props and Events

Declaring props with export let, dispatching custom events, and two-way binding with bind:value.

export let: declaring props

A Svelte component receives props from its parent by declaring exported let variables — the export keyword, normally used for module exports, is repurposed by Svelte's compiler specifically inside a component's <script> block to mean "this variable is a prop passed in from outside":

HTML
<!-- UserCard.svelte -->
<script>
  export let name;
  export let role = "Member"; // default value if the parent doesn't pass one
</script>

<div class="card">
  <h3>{name}</h3>
  <p>{role}</p>
</div>
HTML
<!-- App.svelte -->
<script>
  import UserCard from "./UserCard.svelte";
</script>

<UserCard name="Ada Lovelace" role="Engineer" />
<UserCard name="Grace Hopper" />

role falls back to "Member" for the second <UserCard>, since no role attribute was passed. Like props in React and Vue, a child component should treat an export let prop as belonging to the parent — reassigning it locally works (it's just a let variable), but any change gets overwritten the next time the parent re-renders with its own value, so props are effectively one-way, parent to child, in practice.

Dispatching custom events

Props flow down; a Svelte component reports something happening back up to its parent through a custom event, created with createEventDispatcher:

HTML
<!-- LikeButton.svelte -->
<script>
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function handleClick() {
    dispatch("liked", { timestamp: Date.now() });
  }
</script>

<button on:click={handleClick}>Like</button>
HTML
<!-- App.svelte -->
<script>
  import LikeButton from "./LikeButton.svelte";

  function handleLiked(event) {
    console.log("Liked at", event.detail.timestamp);
  }
</script>

<LikeButton on:liked={handleLiked} />

createEventDispatcher() must be called once at the top level of the component (not inside a function) and returns a dispatch function. Calling dispatch("liked", payload) fires a custom DOM-like event named liked, and the payload arrives in the parent's handler as event.detail — a naming convention borrowed directly from native browser CustomEvents. The parent listens with the same on: directive used for native DOM events like on:click, which keeps the mental model consistent whether the event came from a <button> or from a custom component.

Two-way binding with bind:value

For form inputs specifically, Svelte offers bind:value — a shorthand that keeps a variable and an input's value synchronized in both directions, without manually wiring up an on:input handler yourself:

HTML
<script>
  let name = "";
</script>

<input type="text" bind:value={name} />
<p>Hello, {name || "stranger"}!</p>

Typing in the input updates name immediately (Svelte generates the equivalent of an on:input listener under the hood), and — going the other direction — programmatically reassigning name elsewhere in the script updates the input's displayed value too. bind:value also works on <select>, checkboxes (as bind:checked), and other native form controls, adapting to whichever DOM property makes sense for that element type.

You can expose the same two-way binding pattern on your own components, by combining export let with a dispatched event — this is Svelte's equivalent to Vue's v-model on a custom component:

HTML
<!-- CustomInput.svelte -->
<script>
  export let value = "";
</script>

<input
  type="text"
  {value}
  on:input={(e) => (value = e.target.value)}
/>
HTML
<!-- App.svelte -->
<script>
  import CustomInput from "./CustomInput.svelte";
  let text = "";
</script>

<CustomInput bind:value={text} />
<p>You typed: {text}</p>

bind:value={text} on <CustomInput> works because Svelte recognizes the convention of an exported value prop being reassigned internally, and keeps the parent's text variable synchronized with it automatically — no manual event dispatching required for this specific pattern, unlike the general custom-event case above.

Common mistakes

  • Calling createEventDispatcher() conditionally or inside a function instead of once at the top level of the component's <script> block.
  • Forgetting that a dispatched event's payload arrives as event.detail, not as the payload directly — a common source of undefined bugs when first working with Svelte's custom events.
  • Reassigning an export let prop inside the child and expecting the parent to see the change — it doesn't, unless the child specifically dispatches an event (or uses the bind: convention) to communicate the new value back up.
  • Using bind:value on a plain custom prop that wasn't designed to support it (no corresponding internal reassignment happening) — binding only works when the component's own logic actually updates that exported variable.