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 $lib paths.

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>