Write your first components — .svelte files, props with $props, conditional rendering, each blocks and keys, events, and composition with snippets.
Why: everything on a Svelte screen is a component — one .svelte file holding the logic in <script> and the markup right below it, no wrapper needed. Note: in a SvelteKit app, shared components go in src/lib/components/ and pages live in src/routes/.
<!-- src/lib/components/Welcome.svelte -->
<!-- A component is one .svelte file: logic on top, markup below -->
<script lang="ts">
const name = 'Ada'
const frameworks = 2 + 1
</script>
<section>
<!-- Braces embed any JavaScript expression -->
<h1>Hello, {name}!</h1>
<p>You know {frameworks} frameworks.</p>
</section>
<!-- Use it like a tag anywhere else:
import Welcome from '$lib/components/Welcome.svelte'
<Welcome /> -->Why: props are how a parent passes data down to a child. They are read-only — a component never modifies its own props. Note: $props() hands you all of them at once; destructure, type, and set fallbacks in one line.
<!-- src/lib/components/Badge.svelte -->
<script lang="ts">
// Describe the props as a type — the ? marks an optional one
// = 0 is the fallback when count is omitted
let { label, count = 0 }: { label: string; count?: number } = $props()
</script>
<span>{label}: {count}</span>
<!-- In the parent:
<Badge label="Inbox" count={3} />
<Badge label="Drafts" /> count falls back to 0 -->Why: components constantly show different UI for different data — signed in or not, empty or full, admin or member. {#if} blocks handle all of it, with {:else if} and {:else} for the branches, plus plain ternaries inside braces for small choices.
<script lang="ts">
type User = { name: string; unread: number; isAdmin: boolean }
let user: User | null = { name: 'Ada', unread: 2, isAdmin: false }
</script>
{#if !user}
<p>Please sign in.</p>
{:else}
<!-- Ternaries work inside braces for small choices -->
<h2>{user.isAdmin ? 'Admin Console' : 'Your Inbox'}</h2>
{#if user.unread > 0}
<p>{user.unread} unread messages</p>
{/if}
{/if}Why: rendering arrays is an {#each} block plus a key in parentheses. The key tells Svelte which item is which between updates, so it can reorder instead of rebuilding. Note: use a stable id from your data — the array index breaks reordering, inserts, and input state.
<script lang="ts">
type Task = { id: number; title: string; done: boolean }
const tasks: Task[] = [
{ id: 1, title: 'Learn markup', done: true },
{ id: 2, title: 'Master runes', done: false },
{ id: 3, title: 'Ship an app', done: false },
]
</script>
<ul>
<!-- (task.id) is the key — stable and unique, never the index -->
{#each tasks as task (task.id)}
<li>{task.done ? '[x]' : '[ ]'} {task.title}</li>
{/each}
</ul>Why: interactivity starts with event handlers. In Svelte 5 they are plain properties — onclick, oninput — written in lowercase like HTML attributes. Pass the function itself; onclick={handleClick()} would run immediately on render.
<script lang="ts">
function handleClick() {
console.log('clicked!')
}
</script>
<div>
<!-- Pass the function — don't call it -->
<button onclick={handleClick}>Log</button>
<!-- Wrap in an arrow function when you need to pass arguments -->
<button onclick={() => console.log('saved at', Date.now())}>Save</button>
<!-- e.currentTarget is typed — TypeScript knows .value exists -->
<input
oninput={(e) => console.log(e.currentTarget.value)}
placeholder="Type here"
/>
</div>Why: instead of components with dozens of props, build a frame and let callers fill it. Whatever the caller nests between the tags arrives as the children snippet, and {@render children()} places it — this is the pattern behind every Card, Layout, and Modal you will ever write.
<!-- src/lib/components/Card.svelte — the component owns the frame -->
<script lang="ts">
import type { Snippet } from 'svelte'
// children = whatever the caller nests between the tags
let { title, children }: { title: string; children: Snippet } = $props()
</script>
<section class="card">
<h3>{title}</h3>
{@render children()}
</section>
<!-- In the parent — the caller owns the content:
<Card title="Revenue">
<p>$12,400 this month</p>
<button>Details</button>
</Card> -->