This episode dissects the Svelte template language: variable declarations and interpolation, property binding, event binding, class binding, reactive statements, stores, and conditional and list rendering with #if and #each. These are the core syntaxes used throughout every following episode.

Episode 3 gave you a working project. Episode 4 fills that project with content: the Svelte template language. Every component of yours is a combination of markup, state, and logic — and this episode covers how the three connect through interpolation, bindings, and render directives.
One of Svelte's strengths is that you write views that look like plain HTML, yet reactivity is hidden inside. {nama} updates itself, bind:value links an input to state, and #each renders lists that keep up with changes. There is no abstract API to memorize — the syntax is JavaScript embedded in markup.
In this episode you will learn variable declarations and interpolation, property and event binding, reactive statements, stores, and conditional and list rendering. When you finish, you can build almost any interface view an application needs.
All Svelte state is JavaScript variables in the <script> block. In Svelte 5 with runes, use $state for reactive variables:
<script>
let pesan = $state("Halo Svelte")
let jumlah = $state(3)
</script>
<h1>{pesan}</h1>
<p>Jumlah: {jumlah}</p>let pesan = $state("Halo Svelte") declares reactive state. The curly braces {pesan} in markup are interpolation: the value is rendered as text and updates automatically when state changes. In Svelte 4, you would simply write let pesan = "Halo Svelte" without $state — both are valid, but runes are the future.
To connect form elements to state two-way, Svelte provides bind:. The state value changes as the user types, and the view updates when the state changes from code:
<script>
let nama = $state("")
</script>
<input bind:value={nama} placeholder="Ketik nama" />
<p>Halo, {nama || "anonim"}!</p>bind:value={nama} keeps nama always in sync with the input's content. This is two-way: typing updates the state, and changing the state updates the input. Other common bindings: bind:checked for checkboxes, bind:files for file inputs, and bind:this for direct DOM element references.
In Svelte 5, event handlers are written as element properties. Attach a function or expression directly:
<script>
let count = $state(0)
function tambah() {
count += 1
}
</script>
<button onclick={tambah}>Klik {count}</button>
<button onclick={() => (count = 0)}>Reset</button>onclick={tambah} binds the click event to the tambah function. In Svelte 4 the syntax was on:click={tambah} — still supported, but the property form is the new style. Other events like oninput, onsubmit, and onkeydown follow the same pattern.
To toggle a class reactively, use an object or array inside class::
<script>
let aktif = $state(false)
</script>
<button
class="tombol"
class:aktif
class:mati={!aktif}
onclick={() => (aktif = !aktif)}
>
Status: {aktif ? "aktif" : "mati"}
</button>class:aktif adds the aktif class when its variable value is truthy, and removes it when falsy. The class:mati={!aktif} form uses an expression. This is the most ergonomic way to style elements based on state.
Before runes, $: was the heart of Svelte reactivity. This syntax still works and is useful for derived values:
<script>
let harga = $state(1000)
let pajak = $derived(harga * 0.11)
</script>
<p>Harga: {harga}</p>
<p>Pajak: {pajak}</p>
<p>Total: {harga + pajak}</p>let pajak = $derived(harga * 0.11) recalculates pajak every time harga changes. This is equivalent to $: pajak = harga * 0.11 in Svelte 4. The difference: $derived is only for pure derived values, while $: can contain statements and side effects.
For state used by many components, use a store from svelte/store:
import { writable } from "svelte/store"
export const keranjang = writable([])writable([]) creates a store containing an empty array. Components read its value with the $ prefix — for example $keranjang — and modify it with .set() or .update(). The full store concept is covered in episode 6.
To display a block of markup based on a condition, use #if for the first block and an else block for the second branch:
<script>
let login = $state(false)
</script>
{#if login}
<p>Selamat datang kembali!</p>
{:else}
<button onclick={() => (login = true)}>Login</button>
{/if}{#if login} opens the conditional block and {/if} closes it. Svelte removes unused blocks from the DOM — it does not merely hide them. That makes conditional rendering both efficient and easy to read.
To render an array, use #each with an optional key for accurate list updates:
<script>
let tugas = $state(["Belajar", "Latihan", "Deploy"])
</script>
<ul>
{#each tugas as t, i (t)}
<li>{i + 1}. {t}</li>
{/each}
</ul>{#each tugas as t, i (t)} iterates the tugas array, providing the value t and the index i. The expression (t) is a key that helps Svelte track items as the array changes. Without a key, list reordering can produce incorrect DOM.
Key takeaways:
$state for reactive variables and {expression} for interpolation in markup.bind: makes form properties two-way; onclick binds events in Svelte 5.class:aktif adds or removes a class based on a condition.$derived and $effect replace the majority of $: statements in Svelte 5.#if and #each handle conditional and list rendering.#each a key so list updates stay consistent.In the next episode 5 we will discuss component communication — how components exchange data through props, slots, and event dispatch, including two-way binding with bind: and patterns for reusable component composition. The template syntax from this episode will be your main toolkit.