Components
Components are Svelte SFCs: script, markup, style. Props in, events out, slots for projection — and a compiler that does the diff work for you.
Svelte — components essentials
EXAMPLE
<!-- ===== Counter.svelte ===== -->
<script>
// Props (Svelte 4 style; runes-based Svelte 5 differs)
export let label = 'count';
export let start = 0;
let count = start;
$: doubled = count * 2; // reactive declaration
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function inc() {
count += 1;
dispatch('change', count);
}
</script>
<button on:click={inc}>
{label}: {count} (x2 = {doubled})
</button>
<style>
button { padding: 0.5rem 1rem; border-radius: 8px; }
button:hover { background: #eef; }
/* styles are scoped to this component automatically */
</style>
<!-- ===== App.svelte (using Counter + slots) ===== -->
<script>
import Counter from './Counter.svelte';
let total = 0;
function onChange(e) { total += e.detail; }
</script>
<Counter label="clicks" start={0} on:change={onChange} />
<Counter label="votes" start={5} on:change={onChange} />
<p>Total: {total}</p>
<!-- ===== Slots ===== -->
<!-- Card.svelte -->
<div class="card">
<header><slot name="title">Default title</slot></header>
<div><slot /></div>
<footer><slot name="actions" /></footer>
</div>
<!-- Use Card -->
<Card>
<h2 slot="title">Order #42</h2>
<p>Two items, AUD 49.95</p>
<button slot="actions">Pay</button>
</Card>
<!-- ===== Bind: two-way binding helpers ===== -->
<!-- Parent owns the value; child mutates via bind: -->
<input bind:value={name} />
<Counter bind:start={initial} /> <!-- child prop becomes two-way -->
<!-- ===== Lifecycle ===== -->
<script>
import { onMount, onDestroy, tick } from 'svelte';
onMount(() => {
const t = setInterval(() => count++, 1000);
return () => clearInterval(t); // cleanup
});
onDestroy(() => { /* fallback for non-mount path */ });
async function focusNext() { await tick(); inputEl.focus(); }
</script>
<!-- ===== Stores (cross-component state) ===== -->
<!-- stores.js -->
import { writable, derived } from 'svelte/store';
export const cart = writable([]);
export const total = derived(cart, ($cart) => $cart.reduce((s, l) => s + l.price, 0));
<!-- in any .svelte file -->
<script>
import { cart, total } from './stores.js';
</script>
<p>Items: {$cart.length}, total: {$total}</p>
<button on:click={() => cart.update((c) => [...c, { price: 10 }])}>Add</button>
<!-- ===== Patterns to internalise =====
- export let for props; defaults double as the documentation
- $: for derived state; the compiler tracks deps automatically
- createEventDispatcher + on:event for explicit child-to-parent comms
- Slots for content projection; named slots for layouts
- writable/derived stores for cross-component state; no context API ceremony
===== Pitfalls =====
- Mutating arrays/objects in place doesn't trigger updates -> reassign (arr = [...arr])
- bind: makes prop two-way; remember the parent now owns the rebind
- $: reactive blocks run in declaration order; avoid forward references
- Forgetting cleanup in onMount returns -> intervals/listeners leak
- Mixing stores and prop drilling -> pick a direction per concern
-->
Why it matters
Components are the unit Svelte is built around: small, single-file, scoped styles, compile-time reactivity. Props in, events out, slots for layout — and stores when state crosses trees. The mental model that gets you furthest is "describe the shape; the compiler does the diff".
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<!-- Counter.svelte -->
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>Clicked {count}</button>
Try it Yourself »
Discussion
Loading…