09 - Build a Simple Todo App
Step 1: Create the Project
npx sv create todo-app
cd todo-app
npm install
npm run devSelect 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
.sveltefiles $libis a SvelteKit alias pointing tosrc/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.svelteis 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:
$stateis Svelte 5's reactivity primitive — UI updates automatically when these changetodosis a typed array of todo itemsnewTodoholds 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 stateonsubmit={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 updatesbind:checked={todo.done}— two-way binds checkbox to thedoneproperty
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 devOpen 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.tsload function to learn SvelteKit's server-side data loading