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":
<!-- 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>
<!-- 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:
<!-- LikeButton.svelte -->
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function handleClick() {
dispatch("liked", { timestamp: Date.now() });
}
</script>
<button on:click={handleClick}>Like</button>
<!-- 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:
<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:
<!-- CustomInput.svelte -->
<script>
export let value = "";
</script>
<input
type="text"
{value}
on:input={(e) => (value = e.target.value)}
/>
<!-- 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 ofundefinedbugs when first working with Svelte's custom events. - Reassigning an
export letprop inside the child and expecting the parent to see the change — it doesn't, unless the child specifically dispatches an event (or uses thebind:convention) to communicate the new value back up. - Using
bind:valueon 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.