04 - Props ($props)
What is $props?
$props() declares component props. Replaces export let syntax.
A component is a reusable, self-contained block of UI (a .svelte file) that encapsulates markup, styles, and logic. Props are the inputs a parent passes to a child component to customize its behavior and content.
Svelte 4 (Legacy)
<!-- src/lib/components/Greeting.svelte -->
<script>
export let title; // each prop was a separate export let
export let count = 0; // default value
</script>Svelte 5
<!-- src/lib/components/Greeting.svelte -->
<script>
let { title, count = 0 } = $props(); // all props in one destructure
</script>Basic Usage
<!-- src/lib/components/Card.svelte (child component) -->
<script>
let { title, count } = $props();
</script>
<h1>{title}</h1>
<p>Count: {count}</p><!-- src/routes/+page.svelte (page — uses the Card component) -->
<script>
import Card from '$lib/components/Card.svelte';
</script>
<Card title="Hello" count={5} />Default Values
If a prop isn't passed by the parent, the default value is used.
<!-- src/lib/components/Card.svelte (child component) -->
<script>
let { title = 'Default Title', count = 0 } = $props();
</script><!-- src/routes/+page.svelte (page — uses the Card component) -->
<script>
import Card from '$lib/components/Card.svelte';
</script>
<!-- Uses defaults: title="Default Title", count=0 -->
<Card />
<!-- Overrides defaults -->
<!-- Strings use quotes, non-strings (numbers, booleans, expressions) use {} -->
<Card title="Custom" count={10} />Rest Props
Captures all props not explicitly destructured into a single object. Useful for wrapper components — handle the props you care about and pass everything else through to an underlying element.
<!-- src/lib/components/MyComponent.svelte (child component) -->
<script>
let { title, ...rest } = $props();
</script>
<h1>{title}</h1>
<div {...rest}>Content</div>If a parent passes <MyComponent title="Hi" class="box" id="main" />, then rest would be { class: "box", id: "main" } and those get spread onto the <div>.
Renaming Props
Use destructuring alias syntax when a prop name is a reserved JavaScript keyword (like class) or when you want the internal name to differ from the external one.
<!-- src/lib/components/Box.svelte (child component) -->
<script>
let { class: className = '' } = $props();
</script>
<div class={className}>Content</div>You can't write let { class } = $props() because class is a reserved keyword, so you alias it to className.
Type Safety (TypeScript)
Define an interface for your props and apply it to the $props() destructuring. TypeScript enforces the shape at compile time — missing required props or wrong types produce errors.
<!-- src/lib/components/Child.svelte (child component) -->
<script lang="ts">
interface Props {
title: string;
count?: number; // ? makes it optional
}
let { title, count = 0 }: Props = $props();
</script><!-- src/routes/+page.svelte (page — uses the Child component) -->
<script lang="ts">
import Child from '$lib/components/Child.svelte';
</script>
<!-- ✅ Works — title is required, count is optional -->
<Child title="Hello" />
<Child title="Hello" count={5} />
<!-- ❌ Error — missing required prop 'title' -->
<Child />
<!-- ❌ Error — count expects number, not string -->
<Child title="Hello" count="five" />$state() vs $props() — Internal Data vs External Data
$state() is for internal/private data the component owns. $props() is for external data the parent passes in.
Internal state (component owns the data):
<!-- src/lib/components/UserProfile.svelte (child component) -->
<script lang="ts">
interface User { name: string; age: number; }
let user = $state<User>({ name: 'Alice', age: 25 });
let items = $state<number[]>([1, 2, 3]);
</script>
<p>{user.name} is {user.age} years old</p>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
<button onclick={() => user.age++}>Birthday</button>
<button onclick={() => items.push(items.length + 1)}>Add Item</button><!-- src/routes/+page.svelte (page — no props needed, data is internal) -->
<script lang="ts">
import UserProfile from '$lib/components/UserProfile.svelte';
</script>
<UserProfile />With props (parent passes the data):
<!-- src/lib/components/UserProfile.svelte (child component) -->
<script lang="ts">
interface User { name: string; age: number; }
let { user, items = [1, 2, 3] }: { user: User; items?: number[] } = $props();
</script>
<p>{user.name} is {user.age} years old</p>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul><!-- src/routes/+page.svelte (page — passes data via props) -->
<script lang="ts">
import UserProfile from '$lib/components/UserProfile.svelte';
</script>
<UserProfile user={{ name: "Alice", age: 25 }} />
<UserProfile user={{ name: "Bob", age: 30 }} items={[10, 20]} />Key Points
- Replaces Svelte 4's
export let— all props in one$props()destructure - Use JS destructuring for defaults, rest, and renaming
- Strings use quotes (
title="Hello"), non-strings use braces (count={5}) $state()= internal data,$props()= external data from parent- TypeScript interfaces enforce prop types at compile time
Complete Example
To test in svelte.dev/playground, use
import Child from './Child.svelte'instead of$libpaths.
Child.svelte:
<!-- src/lib/components/Child.svelte (child component — receives props) -->
<script lang="ts">
interface Props {
title: string;
count?: number;
color?: string;
size?: 'small' | 'medium' | 'large';
}
let {
title,
count = 0,
color = 'blue',
size = 'medium'
}: Props = $props();
let doubled = $derived(count * 2);
</script>
<div class="card {size}" style="border-color: {color}">
<h3>{title}</h3>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Color: {color}</p>
<p>Size: {size}</p>
</div>
<style>
.card {
border: 2px solid;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
}
.small { font-size: 12px; }
.medium { font-size: 16px; }
.large { font-size: 20px; }
h3 { margin: 0 0 10px 0; }
</style>Parent.svelte:
<!-- src/routes/+page.svelte (page — passes props to child components) -->
<script lang="ts">
import Child from '$lib/components/Child.svelte';
let count = $state(0);
let color = $state('blue');
let size = $state<'small' | 'medium' | 'large'>('medium');
</script>
<div>
<h2>Props Demo</h2>
<div class="controls">
<button onclick={() => count++}>Increment Count</button>
<button onclick={() => count--}>Decrement Count</button>
<select bind:value={color}>
<option value="blue">Blue</option>
<option value="red">Red</option>
<option value="green">Green</option>
</select>
<select bind:value={size}>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</div>
<Child title="Default Props" />
<Child title="With Count" count={count} />
<Child title="Custom Color" count={count} color={color} />
<Child title="All Props" count={count} color={color} size={size} />
</div>
<style>
div { padding: 20px; font-family: sans-serif; }
h2 { margin-bottom: 20px; }
.controls { margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; }
button, select { padding: 8px 16px; cursor: pointer; }
</style>