09 - Build a Simple Todo App

Step 1: Create the Project

npx sv create todo-app
cd todo-app
npm install
npm run dev

Select the default options when prompted also check tailwindcss. This scaffolds a SvelteKit project and starts the dev server at http://localhost:5173.

Step 2: Navbar Component

Create src/lib/components/Navbar.svelte:

<nav class="flex items-center gap-6 bg-gray-800 px-6 py-4 text-white">
	<a href="/" class="text-xl font-bold">Todo App</a>
	<a href="/" class="hover:text-gray-300">Home</a>
	<a href="/about" class="hover:text-gray-300">About</a>
</nav>

Update src/routes/+layout.svelte:

<script lang="ts">
	import './layout.css';
	import favicon from '$lib/assets/favicon.svg';
	import Navbar from '$lib/components/Navbar.svelte';

	let { children } = $props();
</script>

<svelte:head><link rel="icon" href={favicon} /></svelte:head>
<Navbar />
{@render children()}

Key concepts:

  • Components are reusable pieces of UI in their own .svelte files
  • $lib is a SvelteKit alias pointing to src/lib — use it for shared code and components
  • The layout wraps every page, so the Navbar appears on all routes automatically
  • {@render children()} is where the current page's content gets inserted

Step 3: About Page

Create src/routes/about/+page.svelte:

<div class="mx-auto max-w-md p-8">
	<h1 class="mb-4 text-3xl font-bold">About</h1>
	<p class="text-gray-600">A simple todo app built with SvelteKit, TypeScript, and Tailwind CSS.</p>
</div>

Key concepts:

  • Each folder inside src/routes/ becomes a URL route — about/ maps to /about
  • +page.svelte is the file SvelteKit looks for to render a route

Step 4: Todo State & Functions

Replace everything in src/routes/+page.svelte with a script block:

<script lang="ts">
	let todos = $state<{ id: number; text: string; done: boolean }[]>([]);
	let newTodo = $state('');

	function addTodo() {
		const text = newTodo.trim();
		if (!text) return;
		todos.push({ id: Date.now(), text, done: false });
		newTodo = '';
	}

	function removeTodo(id: number) {
		todos = todos.filter((t) => t.id !== id);
	}
</script>

Key concepts:

  • $state is Svelte 5's reactivity primitive — UI updates automatically when these change
  • todos is a typed array of todo items
  • newTodo holds the current input value

Step 5: Add Todo Form

Below the </script> tag, add the template:

<div class="mx-auto max-w-md p-8">
	<h1 class="mb-6 text-3xl font-bold">Todo App</h1>

	<form onsubmit={addTodo} class="mb-4 flex gap-2">
		<input
			bind:value={newTodo}
			placeholder="What needs to be done?"
			class="flex-1 rounded border px-3 py-2"
		/>
		<button type="submit" class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
			Add
		</button>
	</form>
</div>

Key concepts:

  • bind:value={newTodo} creates two-way binding between input and state
  • onsubmit={addTodo} calls the function on form submit

Step 6: Render the Todo List

Inside the <div>, after </form>, add:

<ul class="space-y-2">
	{#each todos as todo (todo.id)}
		<li class="flex items-center gap-3 rounded border p-3">
			<input type="checkbox" bind:checked={todo.done} class="h-4 w-4" />
			<span class={todo.done ? 'flex-1 text-gray-400 line-through' : 'flex-1'}>
				{todo.text}
			</span>
			<button onclick={() => removeTodo(todo.id)} class="text-red-500 hover:text-red-700" aria-label="Remove todo">

			</button>
		</li>
	{/each}
</ul>

Key concepts:

  • {#each todos as todo (todo.id)} — keyed each block for efficient DOM updates
  • bind:checked={todo.done} — two-way binds checkbox to the done property

Step 7: Completion Counter

After </ul>, still inside the outer <div>:

{#if todos.length > 0}
	<p class="mt-4 text-sm text-gray-500">
		{todos.filter((t) => t.done).length} of {todos.length} completed
	</p>
{/if}

Step 8: Run It

npm run dev

Open http://localhost:5173 — you should see the navbar with Home/About links and the todo app on the home page.

Next Steps

  • Extract the todo item into its own component (TodoItem.svelte) to learn $props
  • Add localStorage persistence so todos survive page refresh
  • Add a +page.server.ts load function to learn SvelteKit's server-side data loading