Building a Todo List with Stores in SvelteKit
Svelte stores provide a simple way to share reactive state across components. In this tutorial, we will build a small Todo List with:
- Adding tasks
- Marking tasks complete
- Editing tasks
- Deleting tasks
- Task statistics
- A live clock
The example uses Svelte + TypeScript and the classic Svelte store APIs.
1. Define the Task Type
Create src/lib/types/task.ts:
export interface Task {
id: number;
title: string;
done: boolean;
}Each task has:
id: a unique identifier.title: the task text.done: whether the task is complete.
2. Create the Task Store
Create src/lib/stores/tasks.ts:
import { writable, readable, derived } from 'svelte/store';
import type { Task } from '$lib/types/task';
export const tasks = writable<Task[]>([]);
export function addTask(title: string) {
tasks.update((list) => [
...list,
{ id: Date.now(), title, done: false }
]);
}
export function toggleTask(id: number) {
tasks.update((list) =>
list.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
)
);
}
export function removeTask(id: number) {
tasks.update((list) => list.filter((t) => t.id !== id));
}
export function updateTask(id: number, newTitle: string) {
tasks.update((list) =>
list.map((t) =>
t.id === id ? { ...t, title: newTitle } : t
)
);
}
export const now = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});
export const stats = derived(tasks, ($tasks) => {
const completed = $tasks.filter((t) => t.done).length;
const pending = $tasks.length - completed;
return { total: $tasks.length, completed, pending };
});The exported helpers do the following:
addTaskadds a task.toggleTaskflips itsdonestate.removeTaskremoves a task by ID.updateTaskreplaces a task title.nowexposes the current time and updates once per second while subscribed.statsderives total, completed, and pending counts fromtasks.
3. Build the Todo List UI
Open src/routes/+page.svelte:
<script lang="ts">
import { tasks, addTask, toggleTask, removeTask, updateTask, now, stats } from '$lib/stores/tasks';
let newTask: string = "";
let editingId: number | null = null;
let editingTitle: string = "";
</script>
<h1>Todo List</h1>
<p>Current time: {$now.toLocaleTimeString()}</p>
<input
bind:value={newTask}
placeholder="New task..."/>
<button
on:click={() => {
if (newTask.trim()) {
addTask(newTask);
newTask = "";
}
}}>
Add
</button>
<ul>
{#each $tasks as task (task.id)}
<li>
<input
type="checkbox"
checked={task.done}
on:change={() => toggleTask(task.id)}/>
{#if editingId === task.id}
<input bind:value={editingTitle}/>
<button
on:click={() => {
updateTask(task.id, editingTitle);
editingId = null;
}}>
Save
</button>
<button on:click={() => (editingId = null)}>
Cancel
</button>
{:else}
<span>{task.title}</span>
<button
on:click={() => {
editingId = task.id;
editingTitle = task.title;
}}>
Edit
</button>
<button on:click={() => removeTask(task.id)}>
Delete
</button>
{/if}
</li>
{/each}
</ul>
<p>
Total: {$stats.total} | Completed: {$stats.completed} | Pending: {$stats.pending}
</p>The page can now add, complete, edit, and remove tasks while automatically reflecting changes from the store.
4. Final Result
The application provides full in-memory CRUD behavior plus two examples of other store types:
readablefor a value updated by an internal producer.derivedfor values calculated from another store.
Because the task list is only stored in memory, it resets when the page is reloaded. You can extend the example with localStorage, a SvelteKit server endpoint, or a backend database.
Conclusion
Classic Svelte stores are a compact way to share reactive state:
- The
Taskinterface keeps the data shape explicit. - The writable
tasksstore owns mutable application state. - The
nowandstatsstores demonstrate readable and derived values. - Components react automatically to store updates through the
$storesubscription syntax.
Svelte 5 also provides newer runes-based state patterns, but stores remain useful for existing projects and for shared reactive values that fit the store contract.