What Are Props?

Props (short for “properties”) are the primary way to pass data from a parent component to a child component in Svelte. Think of props as the inputs to your component—just like a function receives arguments, a component receives props.

In Svelte 4 and earlier, props were declared using export let. While clever, this approach had limitations—it scattered declarations throughout your code and made TypeScript integration challenging. Svelte 5’s $props rune centralizes prop handling into a single, destructurable object that makes your component’s interface immediately visible.

When you use a component in Svelte, every attribute you place on the component tag becomes a prop that the child component can access:

<!-- App.svelte -->
<script>
	import ProductCard from './ProductCard.svelte'
</script>

<!-- These attributes become props inside ProductCard -->
<ProductCard name="Wireless Headphones" price={79.99} inStock={true} />

In Svelte 5, the $props rune is how you access these props inside your component. It returns an object containing all the values passed from the parent:

<!-- ProductCard.svelte -->
<script>
	// $props() returns { name: "Wireless Headphones", price: 79.99, inStock: true }
	let props = $props()
</script>

<div class="product">
	<h2>{props.name}</h2>
	<p>${props.price}</p>
	{#if props.inStock}
		<span class="badge">In Stock</span>
	{/if}
</div>

This simple mechanism powers all parent-to-child communication in Svelte applications.

Why Props Matter

Understanding props is fundamental to building Svelte applications because they define how your components communicate. Well-designed props create:

Clear Component Interfaces: Props declare what data a component needs, making it obvious how to use the component correctly.

Reusable Components: By accepting props, a single component can display different content based on the data it receives.

Predictable Data Flow: Data flows in one direction—from parent to child—making your application easier to understand and debug.

Type Safety: With TypeScript, props can be fully typed, catching errors before your code runs.

Let’s explore how to use $props effectively, starting with the most important pattern: destructuring.

Basic Props with Destructuring

While you can access props as props.name, props.price, etc., this quickly becomes verbose. JavaScript’s destructuring syntax provides a cleaner approach that also serves as documentation for your component’s interface.

<!-- RecipeCard.svelte -->
<script>
	// Destructuring extracts named properties from the props object
	let { title, cookTime, difficulty, ingredients } = $props()
</script>

<article class="recipe-card">
	<h2>{title}</h2>
	<div class="meta">
		<span>{cookTime} mins</span>
		<span>{difficulty}</span>
	</div>
	<p>{ingredients.length} ingredients</p>
</article>

When the parent uses this component:

<!-- App.svelte -->
<script>
	import RecipeCard from './RecipeCard.svelte'
</script>

<RecipeCard
	title="Pasta Carbonara"
	cookTime={25}
	difficulty="Medium"
	ingredients={['pasta', 'eggs', 'pecorino', 'guanciale', 'pepper']}
/>

The destructuring { title, cookTime, difficulty, ingredients } extracts these four values from the props object, making them available as regular variables in your component.

Why Destructuring Is Preferred

Destructuring isn’t just about shorter code—it creates self-documenting components:

Performance Tip

Destructuring props is zero-cost at runtime—the Svelte compiler optimizes it away during build.

<script>
	// Less clear - you must read the template to understand what props exist
	let props = $props();
</script>

<h1>{props.headline} - {props.author}</h1>

<script>
	// PREFERRED: the component's interface is visible immediately
	let { headline, author } = $props();
</script>

<h1>{headline} - {author}</h1>

Anyone reading your component can instantly see what props it expects by looking at the destructuring statement. This explicitness improves maintainability and makes refactoring safer.

Default Values for Props

Often, you want props to have fallback values when the parent doesn’t provide them. JavaScript’s destructuring syntax supports default values with the = operator:

<!-- Alert.svelte -->
<script>
	let { message, type = 'info', dismissible = true, icon = '💡' } = $props()
</script>

<div class="alert alert-{type}">
	<span class="icon">{icon}</span>
	<p>{message}</p>
	{#if dismissible}
		<button class="close">×</button>
	{/if}
</div>

Now the parent can provide only the props they care about:

<!-- All of these work correctly -->
<Alert message="Your changes have been saved" />
<!-- Uses defaults: info type, dismissible, 💡 icon -->

<Alert message="Are you sure?" type="warning" icon="⚠️" />
<!-- Custom type and icon, default dismissible -->

<Alert message="Critical error occurred" type="error" dismissible={false} />
<!-- Custom type, not dismissible, default icon -->

When Defaults Apply

Default values apply only when the prop is undefined. This is important to understand—falsy values like null, false, 0, or empty string "" do NOT trigger defaults:

<script>
	let { quantity = 1, available = true, label = 'Unnamed Item' } = $props()
</script>

<!-- Usage and resulting values -->
<Component />
<!-- quantity: 1, available: true, label: 'Unnamed Item' -->
<Component quantity={0} />
<!-- quantity: 0 (NOT 1!) -->
<Component available={false} />
<!-- available: false (NOT true!) -->
<Component label="" />
<!-- label: '' (NOT 'Unnamed Item'!) -->
<Component quantity={undefined} />
<!-- quantity: 1 (default applies) -->

This behavior is intentional. When a parent explicitly passes 0, false, or an empty string, they mean it. Defaults are truly for when no value is provided at all.

Complex Default Values

Defaults in JavaScript destructuring are not limited to primitive values. You can use objects, arrays, functions, and even complex expressions. This flexibility enables sophisticated component designs:

<!-- DataTable.svelte -->
<script>
	let {
		// Object default - provides a complete configuration
		config = {
			sortable: true,
			filterable: false,
			pageSize: 10,
			showHeader: true,
			stickyHeader: false
		},

		// Array default - defines default columns if none provided
		columns = ['id', 'name', 'value'],

		// Function default - provides default behavior
		onRowClick = (row) => console.log('Row clicked:', row),

		// Function expression for formatting
		formatDate = (date) => new Date(date).toLocaleDateString(),

		// Computed value using an expression
		pageOptions = [10, 25, 50, 100]
	} = $props()
</script>

<table>
	<thead>
		<tr>
			{#each columns as column}
				<th>{column}</th>
			{/each}
		</tr>
	</thead>
	<!-- Table body implementation would use config, formatDate, etc. -->
</table>

This pattern is incredibly powerful for creating components with rich default behavior. The DataTable component above works out of the box with zero configuration, but every aspect can be customized by passing the appropriate prop. This “progressive disclosure of complexity” is a hallmark of well-designed component APIs.

Important: Default Objects Aren’t Reactive

Here is one of the most important—and commonly misunderstood—behaviors in Svelte 5’s prop system: default object values for non-bindable props are not converted to reactive state proxies. This means that if a parent component doesn’t provide a prop and your component uses its default object value, mutations to that object will not trigger reactivity:

<!-- CounterWithDefault.svelte -->
<script>
	let { counts = { value: 0 } } = $props()
</script>

<button
	onclick={() => {
		// ⚠️ CRITICAL: This mutation has NO EFFECT when using the default value!
		// The default object { value: 0 } is a plain JavaScript object,
		// not a reactive proxy, so changes to it don't trigger updates
		counts.value += 1
	}}
>
	Count: {counts.value}
</button>

Let’s trace through exactly what happens in different scenarios:

<!-- Parent.svelte -->
<script>
	import CounterWithDefault from './CounterWithDefault.svelte'

	// This creates a reactive state proxy
	let counts = $state({ value: 0 })
</script>

<!-- Scenario 1: Parent provides reactive state -->
<!-- PREFERRED: Works perfectly - counts is a reactive proxy from parent,
     so mutations trigger updates -->
<CounterWithDefault {counts} />

<!-- Scenario 2: No prop provided, uses default -->
<!-- AVOID: Clicking won't update the display - the default object
     { value: 0 } is not reactive -->
<CounterWithDefault />

This behavior exists for good reasons. Making every default value automatically reactive would have performance implications and could lead to confusing situations where objects are unexpectedly shared across component instances. Instead, Svelte gives you explicit control over reactivity.

How to handle this situation: If your component needs to mutate an object prop internally, you have several options:

  1. Use $bindable to create a two-way binding (covered in detail later)
  2. Create local reactive state derived from the prop:
<script>
	let { initialCounts = { value: 0 } } = $props()

	// Create local reactive state from the prop
	let counts = $state({ ...initialCounts })
</script>

<button onclick={() => counts.value++}>
	Count: {counts.value}
</button>
  1. Use callback props to communicate changes back to the parent:
<script>
	let { counts = { value: 0 }, onCountChange } = $props()
</script>

<button onclick={() => onCountChange?.({ value: counts.value + 1 })}>
	Count: {counts.value}
</button>

Understanding this caveat is essential for avoiding subtle bugs in your Svelte 5 applications. When in doubt, remember: if you need to mutate an object prop, either use $bindable or create local state.

Renaming Props with Destructuring

JavaScript’s destructuring syntax allows you to rename properties as you extract them, and this capability is invaluable when working with props. There are two main scenarios where renaming becomes necessary: when a prop name conflicts with a JavaScript reserved word, and when you want to use a different name internally for clarity.

<!-- NavigationItem.svelte -->
<script>
	let {
		// 'class' is a reserved word in JavaScript, so we rename it to 'className'
		// The parent still passes it as 'class', but we access it as 'className'
		class: className = '',

		// 'for' is also reserved (used in for loops and labels)
		for: htmlFor = '',

		// Sometimes you want a different internal name for clarity
		// Parent passes 'isActive', but internally 'active' is more concise
		isActive: active = false,

		// The 'super' keyword is reserved
		super: superProp = 'default value'
	} = $props()
</script>

<a class="nav-item {className}" class:active for={htmlFor}> </a>

This pattern is especially important when creating wrapper components for HTML elements. HTML has attributes like class, for, and others that collide with JavaScript reserved words. By renaming during destructuring, you maintain a clean API for consumers (they use standard HTML attribute names) while avoiding syntax errors in your implementation.

The syntax originalName: newName = defaultValue might look complex at first, but it breaks down logically: you’re saying “take the property called originalName, call it newName in this scope, and if it doesn’t exist, use defaultValue.”

Required vs Optional Props

Props without default values are implicitly required—if the parent doesn’t provide them, they’ll be undefined:

<script>
	let {
		// Required - no default value
		orderId,
		customerName,

		// Optional - has default value
		notes = ''
	} = $props()
</script>

For TypeScript users, this distinction becomes explicit in your type definitions:

<script lang="ts">
	interface Props {
		orderId: string // Required
		customerName: string // Required
		notes?: string // Optional (note the ?)
	}

	let { orderId, customerName, notes = '' }: Props = $props()
</script>

TypeScript will warn if a parent component uses your component without providing required props.

Passing Different Types of Data

Props can pass any JavaScript value—primitives, objects, arrays, functions, and more:

<!-- Parent.svelte -->
<script>
	import Dashboard from './Dashboard.svelte'

	const user = { name: 'Stan', id: 42 }
	const recentOrders = [
		{ id: 1, total: 59.99 },
		{ id: 2, total: 124.5 }
	]
	const handleLogout = () => console.log('Logging out...')
</script>

<!-- String prop -->
<Dashboard title="Sales Overview" />

<!-- Number prop -->
<Dashboard targetRevenue={50000} />

<!-- Boolean prop -->
<Dashboard showChart={true} />
<Dashboard showChart />
<!-- Shorthand for showChart={true} -->

<!-- Object prop -->
<Dashboard {user} />
<!-- Shorthand for user={user} -->

<!-- Array prop -->
<Dashboard orders={recentOrders} />

<!-- Function prop -->
<Dashboard onLogout={handleLogout} />

The Shorthand Syntax

When the prop name matches a variable name, you can use the shorthand {variable} instead of name={variable}:

<script>
	let title = 'My Dashboard'
	let metrics = [
		/* ... */
	]
	let isLoading = false
</script>

<!-- These are equivalent -->
<Widget {title} {metrics} {isLoading} />
<Widget {title} {metrics} {isLoading} />

This shorthand keeps your templates cleaner when variable names match prop names.

Conditional Defaults Based on Other Props

Sometimes you need a default value that depends on another prop. Unfortunately, you cannot directly reference other props within the destructuring defaults—JavaScript evaluates all defaults before the destructuring assignment completes. However, you can elegantly handle this pattern using $derived:

<!-- Avatar.svelte -->
<script>
	let {
		name,
		size = 'medium',
		src = undefined // Explicitly undefined, we'll compute the real default
	} = $props()

	// Use $derived to compute a default based on another prop
	// This creates a reactive value that updates if 'name' or 'src' changes
	let imageSrc = $derived(
		src ?? `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`
	)

	// Size mapping for different avatar sizes
	const sizeMap = {
		small: { class: 'w-8 h-8', fontSize: '0.75rem' },
		medium: { class: 'w-12 h-12', fontSize: '1rem' },
		large: { class: 'w-16 h-16', fontSize: '1.25rem' }
	}

	let sizeConfig = $derived(sizeMap[size])
</script>

<img src={imageSrc} alt="Avatar for {name}" class="rounded-full {sizeConfig.class}" />

In this example, the imageSrc is computed based on whether src was provided. If the parent passes a src prop, that value is used. If not, we generate a placeholder avatar URL based on the user’s name. The $derived rune ensures this computation is reactive—if either src or name changes, imageSrc updates automatically.

This pattern keeps your props declaration clean (you can see at a glance what props the component accepts) while allowing sophisticated default logic in the component body.

Layered Defaults for Complex Configuration

When building components with many configuration options, a common pattern is to merge user-provided configuration with defaults. This allows users to override only the specific options they care about:

<!-- Chart.svelte -->
<script>
	// Default configuration object
	const defaultConfig = {
		animation: {
			enabled: true,
			duration: 300,
			easing: 'ease-out'
		},
		tooltip: {
			enabled: true,
			format: (value) => value.toLocaleString()
		},
		legend: {
			position: 'bottom',
			visible: true
		},
		colors: ['#3b82f6', '#ef4444', '#22c55e', '#f59e0b']
	}

	let { data, config = {} } = $props()

	// Deep merge user config with defaults
	// This allows users to override nested properties without
	// having to specify the entire config object
	let mergedConfig = $derived({
		animation: { ...defaultConfig.animation, ...config.animation },
		tooltip: { ...defaultConfig.tooltip, ...config.tooltip },
		legend: { ...defaultConfig.legend, ...config.legend },
		colors: config.colors ?? defaultConfig.colors
	})
</script>

Now users can customize just the parts they care about:

<!-- Only customize animation duration and legend position -->
<Chart
	{data}
	config={{
		animation: { duration: 500 },
		legend: { position: 'right' }
	}}
/>

This layered approach to defaults is particularly valuable for complex components where the full configuration object might have dozens of options. Users don’t need to know or specify every option—they only provide the customizations they need, and sensible defaults handle everything else.

Understanding Prop Reactivity

Props in Svelte are reactive—when a parent updates a value, the child component automatically receives the new value:

<!-- Parent.svelte -->
<script>
	import TemperatureDisplay from './TemperatureDisplay.svelte';

	let celsius = $state(20);
</script>

<input type="range" min="-20" max="50" bind:value={celsius} />
<p>Slider: {celsius}°C</p>

<TemperatureDisplay {celsius} />

<!-- TemperatureDisplay.svelte -->
<script>
	let { celsius } = $props();

	let fahrenheit = $derived((celsius * 9/5) + 32);
</script>

<!-- These update automatically when parent's celsius changes -->
<p>{celsius}°C = {fahrenheit.toFixed(1)}°F</p>

Every time you move the slider in the parent, the child’s display updates too. This reactivity is automatic—you don’t need to do anything special.

One-Way Data Flow

Props Are Read-Only in Svelte 5

Props received from $props() are read-only. Attempting to assign directly to a destructured prop will produce a Svelte runtime warning. Use callback props to signal intent upward, or $bindable for genuine two-way synchronisation.

By default, data flows in one direction: parent to child. Props received from $props() are read-only — Svelte 5 will warn at runtime if you try to assign to them directly. If you need to communicate a change back to the parent, you use either a callback prop or $bindable (covered later).

The following example demonstrates the correct pattern for signalling intent upward through a callback:

<!-- VolumeControl.svelte -->
<script>
	let { volume, onVolumeChange } = $props()
</script>

<!-- Preferred: signal intent to the parent, let it own the state -->
<button onclick={() => onVolumeChange?.(volume + 1)}>
	Increase volume: {volume}
</button>
<!-- App.svelte -->
<script>
	import VolumeControl from './VolumeControl.svelte'

	let volume = $state(50)
</script>

<VolumeControl {volume} onVolumeChange={(v) => (volume = v)} />

This one-way flow makes applications easier to understand—you always know where data comes from. When you need two-way communication, Svelte provides the $bindable rune (covered in the Advanced Props Patterns article).

Passing Event Handlers as Props

In Svelte 5, event handlers are simply function props. Pass them like any other prop:

<!-- Parent.svelte -->
<script>
	import SearchBox from './SearchBox.svelte';

	function handleSearch(query) {
		console.log('Searching for:', query);
	}

	function handleFocus() {
		console.log('Search focused');
	}
</script>

<SearchBox
	onsearch={handleSearch}
	onfocus={handleFocus}
/>

<!-- SearchBox.svelte -->
<script>
	let { onsearch, onfocus } = $props();

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

<input
	type="search"
	bind:value={query}
	{onfocus}
	onkeydown={(e) => e.key === 'Enter' && onsearch?.(query)}
/>

The parent provides the handler functions, and the child attaches them to the appropriate elements. This pattern gives parents full control over how events are handled.

Calling Handlers Safely

When a handler prop is optional, use optional chaining to call it safely. The ?.() syntax calls the function only if it exists — no conditional wrapper needed:

<script>
	let { onselect } = $props()
</script>

<button onclick={() => onselect?.({ id: 1, name: 'Item' })}> Select Item </button>

TypeScript Integration

TypeScript and $props work together seamlessly. Add lang="ts" to your script tag and annotate your props:

<script lang="ts">
	interface Props {
		productId: string
		quantity: number
		notes?: string // Optional
		onAddToCart?: (productId: string, qty: number) => void
	}

	let { productId, quantity, notes = '', onAddToCart }: Props = $props()
</script>

<div class="product-controls">
	<p>Product: {productId}, Qty: {quantity}</p>
	{#if notes}
		<p>Notes: {notes}</p>
	{/if}
	<button onclick={() => onAddToCart?.(productId, quantity)}> Add to Cart </button>
</div>

TypeScript provides:

Autocomplete: Your editor suggests valid props when using the component.

Error Detection: Catch typos and type mismatches at compile time.

Documentation: The interface serves as living documentation of your component’s API.

Inline Type Annotations

For simpler components, you can inline the type:

<script lang="ts">
	let {
		label,
		value
	}: {
		label: string
		value: number
	} = $props()
</script>

However, named interfaces are generally preferred for readability and reusability.

The children Prop and Snippets

When you place content between a component’s opening and closing tags, it becomes available as a special children prop — a typed “snippet” that Svelte passes automatically. You render it with {@render children?.()}. The ?.() safely handles cases where the parent provides no children at all.

This replaces the old <slot> element from Svelte 4 entirely. There is no <slot> in Svelte 5.

<!-- Panel.svelte -->
<script>
	let { children } = $props()
</script>

<div class="panel">
	{@render children?.()}
</div>
<!-- App.svelte -->
<script>
	import Panel from './Panel.svelte'
</script>

<Panel>
	<h2>Welcome Back!</h2>
	<p>Your dashboard is ready.</p>
</Panel>

The content you nest inside <Panel> is compiled into a snippet and passed to the component as children. The component decides where — and whether — to render it.

Children with TypeScript

To type the children prop, import the Snippet type:

<script lang="ts">
	import type { Snippet } from 'svelte'

	interface Props {
		heading: string
		children?: Snippet
	}

	let { heading, children }: Props = $props()
</script>

<section>
	<h1>{heading}</h1>
	{@render children?.()}
</section>

Generating Unique IDs with $props.id()

Use $props.id() for Accessible ID Pairs

$props.id() generates an ID that is stable across server and client rendering and unique to each component instance. This prevents hydration mismatches and ID collisions when a component is used multiple times on the same page.

Accessibility often requires linking elements with unique IDs. The $props.id() function generates IDs that are unique to each component instance and stable across server/client rendering:

<script>
	let { label, hint } = $props()

	// Generates a unique ID for this component instance
	const uid = $props.id()
</script>

<div class="field">
	<label for="{uid}-input">{label}</label>
	<input id="{uid}-input" aria-describedby="{uid}-hint" />
	{#if hint}
		<p id="{uid}-hint" class="hint">{hint}</p>
	{/if}
</div>

Each instance of this component gets its own unique IDs, preventing conflicts when the component is used multiple times on a page.

Common Patterns and Best Practices

1: Configuration Objects

For components with many options, consider grouping related props into a configuration object:

<script>
	let {
		items,
		options = {
			sortable: true,
			searchable: false,
			pageSize: 10
		}
	} = $props()

	// Merge with defaults for partial overrides
	let config = $derived({
		sortable: true,
		searchable: false,
		pageSize: 10,
		...options
	})
</script>

This allows users to override only specific options:

<ItemList {items} options={{ pageSize: 25 }} />

2: Computed Values from Props

Use $derived to compute values based on props:

<script>
	let { items, taxRate } = $props()

	let subtotal = $derived(items.reduce((sum, item) => sum + item.price, 0))
	let tax = $derived(subtotal * taxRate)
	let total = $derived(subtotal + tax)
</script>

<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>

Derived values update automatically when their source props change.

3: Validating Props at Runtime

While TypeScript catches type errors at compile time, you might need runtime validation for dynamic data:

<script>
	let { minValue, maxValue, current } = $props()

	// Validate and warn about invalid states
	// $effect runs after the component is mounted; use it only for side effects
	$effect(() => {
		if (minValue > maxValue) {
			console.warn(`Invalid range: min (${minValue}) > max (${maxValue})`)
		}
	})

	// Clamp current value to valid range
	let clampedValue = $derived(Math.min(Math.max(current, minValue), maxValue))
</script>

<input type="range" min={minValue} max={maxValue} value={clampedValue} />

Common Mistakes to Avoid

1: Mutating Object Props

Svelte 5 props are read-only. Assigning directly to a destructured prop or mutating it will produce a runtime warning and won’t propagate back to the parent. Use a callback prop to communicate intent upward instead:

<!-- Avoid -->
<script>
	let { settings } = $props()

	function toggleDarkMode() {
		settings.darkMode = !settings.darkMode // Warning: prop is read-only
	}
</script>
<!-- Preferred -->
<script>
	let { settings, onSettingsChange } = $props()

	function toggleDarkMode() {
		onSettingsChange({ ...settings, darkMode: !settings.darkMode })
	}
</script>

2: Forgetting Default Value Behavior

Remember that defaults only apply for undefined:

<script>
	// If parent passes quantity={0}, quantity will be 0, not 1!
	let { quantity = 1 } = $props()
</script>

3: Not Using TypeScript for Complex Props

For any non-trivial component, TypeScript catches typos and type mismatches before they reach the browser. A misspelled prop name silently becomes undefined at runtime — TypeScript surfaces it immediately at the call site:

<!-- ProductCard.svelte -->
<script lang="ts">
	interface Props {
		name: string
	}
	let { name }: Props = $props()
</script>
<!-- App.svelte -->
<!-- TypeScript error: 'naem' does not exist in type Props -->
<ProductCard naem="Widget" />

<!-- Correct -->
<ProductCard name="Widget" />

Key Takeaways

The $props rune is your gateway to component communication in Svelte 5:

  • Use destructuring to create clear, self-documenting component interfaces
  • Provide defaults for optional props to make components flexible
  • Rename props when needed to avoid reserved word conflicts
  • Remember reactivity flows one way—parent to child by default
  • Pass event handlers as function props for parent-controlled behavior
  • Add TypeScript for type safety and better developer experience
  • Use $props.id() for generating accessible unique IDs

For more advanced patterns—including rest props, bidirectional binding with $bindable, generic components, and complex TypeScript patterns—see the Advanced Props Patterns article.


See Also

Official Documentation

External Resources