The foundation of component communication
Props are how we pass data into Svelte components, and they're fundamental to building reusable, composable interfaces. In Svelte 5, the $props rune completely replaces the older export let syntax, giving us a cleaner, more powerful way to define and manage component inputs. This practical Svelte & SvelteKit guide walks you through everything you need to know: destructuring props with defaults, forwarding attributes, enabling two-way binding, and handling callbacks—all patterns we use daily in production applications.
$props — Destructure props with defaults
The $props rune is your primary tool for declaring what data a component accepts. Unlike the old export let, it combines declaration and destructuring into one clean expression, reducing boilerplate while making the contract explicit.
// Button.svelte
let { title, count = 0 } = $props();
<button>
{title}
{#if count > 0}
<span>{count}</span>
{/if}
</button>Here, title is required, and count defaults to 0 if not provided. The parent passes them as attributes:
// App.svelte
<Button title="Click me" count={5} />
<Button title="Submit" /> {/* count is 0 */}You can also be fully explicit with TypeScript types:
let {
title,
count = 0,
variant = 'primary'
}: {
title: string;
count?: number;
variant?: 'primary' | 'secondary'
} = $props();In a real SvelteKit application, this is invaluable. We recently built a product listing page where card components accepted title, price, image URL, and an optional discount percentage. By using defaults and TypeScript, we caught misconfigurations at development time rather than runtime.
Rest props — Forward attributes
Often you want to accept props you know about, then pass any remaining attributes directly to a DOM element. This is where rest props shine, especially when building wrapper components.
// Input.svelte
let {
name,
placeholder = 'Enter text...',
...rest
} = $props();
<input
{name}
{placeholder}
{...rest}
/>Now the parent can pass any standard input attributes:
<Input
name="email"
type="email"
required
autocomplete="off"
class="form-input"
maxlength={50}
/>The ...rest captures type, required, autocomplete, class, and maxlength, forwarding them all to the underlying input. This pattern is essential in component libraries where you don't want to maintain an ever-growing list of passthrough props.
One team we worked with built a form system where most input components needed this flexibility. Rather than duplicating attributes across Button, TextInput, Checkbox, and Select, we used rest props everywhere. It reduced maintenance burden significantly and made onboarding new form components trivial.
$bindable — Enable two-way binding
By default, props flow one direction: parent to child. But sometimes you need the child to update a parent's state. The $bindable rune enables two-way binding on a specific prop, making it work just like bind: directives on DOM elements.
// Modal.svelte
let {
open = $bindable(false),
title
} = $props();
<dialog open={open}>
<h2>{title}</h2>
<button onclick={() => open = false}>
Close
</button>
</dialog>The parent binds to it:
// App.svelte
let showDialog = false;
<button onclick={() => showDialog = true}>
Open Dialog
</button>
<Modal bind:open={showDialog} title="Confirm action" />When the child closes the modal (setting open = false), the parent's showDialog updates automatically. This eliminates the need for event dispatching when the only intent is to synchronize state.
In a SvelteKit CRM we built, search filters used $bindable extensively. Users could interact with filters in a sidebar component, and the parent page automatically tracked all filter states without explicit event handlers. It felt natural and reduced the complexity of managing UI state across multiple components.
Callbacks — Pass functions instead of dispatching events
While Svelte events exist, passing functions as props is often cleaner, especially for custom actions or integrations. Functions are first-class citizens in JavaScript, and they're explicit about intent.
// SearchBar.svelte
let {
onSearch,
placeholder = 'Search...'
} = $props();
let query = $state('');
function handleSearch() {
onSearch(query);
}
<input
bind:value={query}
{placeholder}
onkeydown={(e) => {
if (e.key === 'Enter') handleSearch();
}}
/>
<button onclick={handleSearch}>Search</button>The parent provides the callback:
// App.svelte
async function handleSearch(query) {
const results = await fetch(`/api/search?q=${query}`);
// handle results
}
<SearchBar onSearch={handleSearch} />This pattern is particularly powerful in SvelteKit layouts and pages where you control data fetching. A component doesn't need to know about API endpoints or server-side logic—it just calls the function you give it. We use this extensively in our e-commerce projects: product filters call a callback that triggers a new server load, pagination buttons call a callback to load the next page.
You can also type callback props strictly:
let {
onSearch,
onError = undefined
}: {
onSearch: (query: string) => void | Promise<void>;
onError?: (error: Error) => void;
} = $props();Common mistakes
Mutating props directly. Props are reactive, but they should be treated as immutable. If you need to transform a prop, use a derived state:
// ❌ Wrong
let { count } = $props();
count = count + 1; // Don't do this
// ✅ Right
let { count } = $props();
let doubled = $derived(count * 2);Forgetting to destructure with defaults. If you declare let { value } = $props() without a default and the parent doesn't pass value, you'll get undefined. Always provide sensible defaults:
let {
items = [],
loading = false
} = $props();Overusing $bindable. Two-way binding is convenient but can make data flow harder to trace. Use it when state truly belongs to both parent and child; otherwise, rely on callbacks or parent-managed state.
Not typing callback props. Without types, you won't know what arguments to pass or what the function returns. Be explicit, especially in a team environment.
Key takeaways
- Use
$props()to declare component inputs with defaults and destructuring in one step, replacing the olderexport letsyntax. - Use rest props (
...rest) to forward unknown attributes to child elements, keeping components flexible and reducing boilerplate. - Use
$bindable()on specific props when you need true two-way synchronization between parent and child state. - Pass functions as props instead of dispatching custom events—it's cleaner, more explicit, and integrates naturally with SvelteKit's server logic.
- Always provide TypeScript types for props, especially callbacks, to catch integration errors early and make your components self-documenting.
- Treat props as immutable; derive new values instead of modifying them directly.






Comments
Loading comments…