
Building a Modern Todo App with Svelte: Complete Step-by-Step Guide
Svelte has revolutionized frontend development with its compile-time approach and minimal runtime overhead. This comprehensive guide will walk you through building a modern, feature-rich todo application using Svelte and TypeScript, covering everything from project setup to advanced features and deployment.
1. Why Choose Svelte for Todo Applications?
Svelte offers several advantages that make it ideal for building todo applications. Its compile-time approach means smaller bundle sizes, better performance, and a more intuitive development experience. The reactive nature of Svelte makes state management straightforward, while TypeScript integration provides excellent type safety.
1.1 Key Svelte Features for Todo Apps
Svelte provides several features that make todo development more efficient:
- Reactive Statements: Automatic reactivity without complex state management
- Small Bundle Size: Compile-time optimization results in smaller applications
- TypeScript Support: Excellent TypeScript integration for better development experience
- Component-Based Architecture: Modular and reusable components
- Built-in Animations: Easy-to-implement transitions and animations
2. Project Setup and Configuration
Setting up a Svelte project requires careful configuration of build tools and dependencies. Let's start with the essential setup files.
2.1 Package Configuration
{
"name": "todo-svelte",
"version": "1.0.0",
"description": "A modern todo app built with Svelte",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tsconfig/svelte": "^5.0.0",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.2",
"vite": "^5.0.0"
},
"dependencies": {
"lucide-svelte": "^0.294.0"
}
}
2.2 Vite Configuration
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [svelte()],
server: {
port: 3000,
open: true
}
})
2.3 TypeScript Configuration
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}
3. Type Definitions and Interfaces
Defining proper TypeScript interfaces is crucial for type safety and better development experience. Here's how to structure your todo types:
// src/types/todo.ts
export interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: Date;
priority: 'low' | 'medium' | 'high';
category?: string;
}
export type TodoFilter = 'all' | 'active' | 'completed';
4. State Management with Svelte Stores
Svelte's built-in stores provide a powerful and simple way to manage application state. For a todo app, we'll use writable stores to manage todos and filters.
// src/stores/todoStore.ts
import { writable } from 'svelte/store';
import type { Todo, TodoFilter } from '../types/todo';
export const todoStore = writable([]);
export const filterStore = writable('all');
// Helper functions for todo operations
export const todoActions = {
add: (text: string, priority: Todo['priority'] = 'medium', category?: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text: text.trim(),
completed: false,
createdAt: new Date(),
priority,
category
};
todoStore.update(todos => [newTodo, ...todos]);
},
toggle: (id: string) => {
todoStore.update(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
},
update: (id: string, updates: Partial) => {
todoStore.update(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
)
);
},
delete: (id: string) => {
todoStore.update(todos => todos.filter(todo => todo.id !== id));
},
clearCompleted: () => {
todoStore.update(todos => todos.filter(todo => !todo.completed));
},
setFilter: (filter: TodoFilter) => {
filterStore.set(filter);
}
};
5. Main Application Component
The main App component serves as the entry point and orchestrates the todo application. It handles localStorage persistence and coordinates between components.
<script lang="ts">
import { onMount } from 'svelte';
import TodoList from './components/TodoList.svelte';
import TodoForm from './components/TodoForm.svelte';
import TodoStats from './components/TodoStats.svelte';
import { todoStore, todoActions } from './stores/todoStore';
import type { Todo } from './types/todo';
let todos: Todo[] = [];
onMount(() => {
// Load todos from localStorage
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
todos = JSON.parse(savedTodos);
todoStore.set(todos);
}
});
// Subscribe to store changes and save to localStorage
todoStore.subscribe((value) => {
todos = value;
localStorage.setItem('todos', JSON.stringify(value));
});
function handleAdd(event: CustomEvent) {
todoActions.add(event.detail.text, event.detail.priority, event.detail.category);
}
</script>
<main class="app">
<div class="container">
<header class="header">
<h1>✨ Modern Todo</h1>
<p>Stay organized and boost your productivity</p>
</header>
<div class="content">
<TodoForm on:add={handleAdd} />
<TodoStats />
<TodoList />
</div>
</div>
</main>
6. Building Core Components
Now let's create the essential components that make up our todo application. Each component follows Svelte's component-based architecture.
6.1 Todo Form Component
The TodoForm component handles adding new todos with advanced options like priority and category selection.
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Plus, Calendar, Tag } from 'lucide-svelte';
import type { Todo } from '../types/todo';
const dispatch = createEventDispatcher<{
add: Todo;
}>();
let text = '';
let priority: Todo['priority'] = 'medium';
let category = '';
let showAdvanced = false;
function handleSubmit() {
if (text.trim()) {
const newTodo: Todo = {
id: crypto.randomUUID(),
text: text.trim(),
completed: false,
createdAt: new Date(),
priority,
category: category.trim() || undefined
};
dispatch('add', newTodo);
text = '';
priority = 'medium';
category = '';
showAdvanced = false;
}
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
</script>
<form class="todo-form" on:submit|preventDefault={handleSubmit}>
<div class="input-group">
<div class="text-input-wrapper">
<input
type="text"
bind:value={text}
on:keydown={handleKeydown}
placeholder="What needs to be done?"
class="text-input"
required
/>
<button type="submit" class="add-button" disabled={!text.trim()}>
<Plus size={20} />
</button>
</div>
<button
type="button"
class="advanced-toggle"
on:click={() => showAdvanced = !showAdvanced}
>
<Calendar size={16} />
Advanced
</button>
</div>
{#if showAdvanced}
<div class="advanced-options">
<div class="option-group">
<label for="priority">Priority:</label>
<select id="priority" bind:value={priority} class="select-input">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="option-group">
<label for="category">Category:</label>
<div class="category-input-wrapper">
<Tag size={16} />
<input
type="text"
id="category"
bind:value={category}
placeholder="Work, Personal, Shopping..."
class="category-input"
/>
</div>
</div>
</div>
{/if}
</form>
6.2 Todo Stats Component
The TodoStats component displays progress statistics and completion rates using derived stores.
<script lang="ts">
import { derived } from 'svelte/store';
import { todoStore } from '../stores/todoStore';
import { TrendingUp, Clock, CheckCircle } from 'lucide-svelte';
// Derived stores for statistics
const stats = derived(todoStore, ($todos) => {
const total = $todos.length;
const completed = $todos.filter(todo => todo.completed).length;
const active = total - completed;
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, active, progress };
});
</script>
{#if $stats.total > 0}
<div class="stats">
<div class="stat-item">
<div class="stat-icon">
<TrendingUp size={20} />
</div>
<div class="stat-content">
<div class="stat-value">{$stats.progress}%</div>
<div class="stat-label">Progress</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<Clock size={20} />
</div>
<div class="stat-content">
<div class="stat-value">{$stats.active}</div>
<div class="stat-label">Active</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<CheckCircle size={20} />
</div>
<div class="stat-content">
<div class="stat-value">{$stats.completed}</div>
<div class="stat-label">Completed</div>
</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {$stats.progress}%"></div>
</div>
{/if}
6.3 Todo Item Component
The TodoItem component represents individual todo items with edit, delete, and completion functionality.
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Check, Trash2, Edit3, Flag, Tag } from 'lucide-svelte';
import { todoActions } from '../stores/todoStore';
import type { Todo } from '../types/todo';
export let todo: Todo;
const dispatch = createEventDispatcher<{
edit: Todo;
}>();
let isEditing = false;
let editText = todo.text;
function toggleComplete() {
todoActions.toggle(todo.id);
}
function deleteTodo() {
todoActions.delete(todo.id);
}
function startEdit() {
isEditing = true;
editText = todo.text;
}
function saveEdit() {
if (editText.trim()) {
todoActions.update(todo.id, { text: editText.trim() });
isEditing = false;
}
}
function cancelEdit() {
isEditing = false;
editText = todo.text;
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
saveEdit();
} else if (event.key === 'Escape') {
cancelEdit();
}
}
function getPriorityColor(priority: Todo['priority']) {
switch (priority) {
case 'high': return '#ef4444';
case 'medium': return '#f59e0b';
case 'low': return '#10b981';
default: return '#6b7280';
}
}
</script>
<div class="todo-item" class:completed={todo.completed}>
<div class="todo-content">
<button
class="checkbox"
class:checked={todo.completed}
on:click={toggleComplete}
aria-label={todo.completed ? 'Mark as incomplete' : 'Mark as complete'}
>
{#if todo.completed}
<Check size={16} />
{/if}
</button>
<div class="todo-text">
{#if isEditing}
<input
type="text"
bind:value={editText}
on:keydown={handleKeydown}
on:blur={saveEdit}
class="edit-input"
autofocus
/>
{:else}
<span class="text" class:completed={todo.completed}>
{todo.text}
</span>
{/if}
<div class="todo-meta">
{#if todo.category}
<span class="category">
<Tag size={12} />
{todo.category}
</span>
{/if}
<span class="priority" style="color: {getPriorityColor(todo.priority)}">
<Flag size={12} />
{todo.priority}
</span>
</div>
</div>
</div>
<div class="todo-actions">
<button
class="action-button edit-button"
on:click={startEdit}
aria-label="Edit todo"
>
<Edit3 size={16} />
</button>
<button
class="action-button delete-button"
on:click={deleteTodo}
aria-label="Delete todo"
>
<Trash2 size={16} />
</button>
</div>
</div>
7. Advanced Features and Enhancements
To make your Svelte todo app stand out, consider implementing these advanced features:
7.1 Derived Stores for Filtering
Use derived stores to create reactive filtered views of your todos:
// Derived store that filters todos based on current filter
const filteredTodos = derived(
[todoStore, filterStore],
([$todos, $filter]) => {
switch ($filter) {
case 'active':
return $todos.filter(todo => !todo.completed);
case 'completed':
return $todos.filter(todo => todo.completed);
default:
return $todos;
}
}
);
7.2 Local Storage Persistence
Implement automatic persistence to localStorage for data persistence across sessions:
// Subscribe to store changes and save to localStorage
todoStore.subscribe((value) => {
todos = value;
localStorage.setItem('todos', JSON.stringify(value));
});
// Load todos from localStorage on mount
onMount(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
todos = JSON.parse(savedTodos);
todoStore.set(todos);
}
});
7.3 Keyboard Shortcuts
Add keyboard shortcuts for better user experience:
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSubmit();
}
}
// In edit mode
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
saveEdit();
} else if (event.key === 'Escape') {
cancelEdit();
}
}
8. Styling and CSS Architecture
Svelte's scoped CSS makes styling components straightforward and maintainable. Here are some key styling patterns:
8.1 Component-Scoped Styles
<style>
.todo-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
transition: all 0.2s ease;
}
.todo-item:hover {
border-color: #d1d5db;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.todo-item.completed {
opacity: 0.7;
}
.checkbox {
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkbox.checked {
background: #667eea;
border-color: #667eea;
color: white;
}
</style>
8.2 Responsive Design
@media (max-width: 640px) {
.todo-item {
padding: 0.75rem;
gap: 0.75rem;
}
.todo-actions {
opacity: 1;
}
.todo-meta {
flex-direction: column;
gap: 0.25rem;
}
}
9. Testing and Quality Assurance
Testing is crucial for maintaining code quality. Here's how to test your Svelte todo app:
9.1 Type Checking
Svelte provides built-in type checking with svelte-check:
// Add to package.json scripts
"check": "svelte-check --tsconfig ./tsconfig.json"
// Run type checking
npm run check
9.2 Component Testing
Test your components using Svelte Testing Library:
import { render, fireEvent } from '@testing-library/svelte';
import TodoItem from './TodoItem.svelte';
describe('TodoItem', () => {
const mockTodo = {
id: '1',
text: 'Test todo',
completed: false,
createdAt: new Date(),
priority: 'medium' as const
};
it('should render todo text', () => {
const { getByText } = render(TodoItem, { props: { todo: mockTodo } });
expect(getByText('Test todo')).toBeInTheDocument();
});
it('should toggle completion when checkbox is clicked', async () => {
const { getByRole } = render(TodoItem, { props: { todo: mockTodo } });
const checkbox = getByRole('button', { name: /mark as complete/i });
await fireEvent.click(checkbox);
// Add assertions for completion state
});
});
10. Performance Optimization
Svelte's compile-time approach already provides excellent performance, but here are additional optimizations:
10.1 Reactive Statements
Use reactive statements for efficient updates:
// Reactive statement for derived values
$: progress = total > 0 ? Math.round((completed / total) * 100) : 0;
// Reactive statement for filtered todos
$: filteredTodos = todos.filter(todo => {
switch (currentFilter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});
10.2 Keyed Each Blocks
Use keyed each blocks for efficient list rendering:
{#each $filteredTodos as todo (todo.id)}
<TodoItem {todo} />
{/each}
10.3 Conditional Rendering
Use conditional rendering to avoid unnecessary DOM updates:
{#if $stats.total > 0}
<div class="stats">
<!-- Stats content -->
</div>
{:else}
<div class="empty-state">
<!-- Empty state content -->
</div>
{/if}
11. Deployment and Build Optimization
Once your Svelte todo app is complete, optimize it for production deployment:
11.1 Production Build
# Build for production
npm run build
# Preview production build
npm run preview
11.2 Build Optimization
- Code Splitting: Vite automatically handles code splitting
- Tree Shaking: Unused code is automatically eliminated
- Minification: CSS and JavaScript are minified for production
- Asset Optimization: Images and other assets are optimized
11.3 Deployment Options
Deploy your Svelte todo app to various platforms:
- Vercel: Zero-config deployment with automatic builds
- Netlify: Drag-and-drop deployment with form handling
- GitHub Pages: Free hosting for open-source projects
- Firebase Hosting: Google's hosting platform with CDN
12. Best Practices and Tips
Follow these best practices to ensure your Svelte todo app is maintainable and scalable:
- Use TypeScript: Leverage TypeScript for better type safety and developer experience
- Component Composition: Break down complex components into smaller, reusable pieces
- Store Management: Use Svelte stores for global state management
- Reactive Statements: Use reactive statements for derived state
- Event Handling: Use proper event handling patterns and avoid inline functions
- Accessibility: Ensure your app is accessible with proper ARIA labels and keyboard navigation
- Performance: Use keyed each blocks and conditional rendering for optimal performance
13. Future Enhancements
Consider these enhancements to make your todo app even more powerful:
13.1 Advanced Features
- Due Dates and Reminders: Add date pickers and notification systems
- Drag and Drop: Implement reordering with Svelte's transition system
- Search and Filter: Add advanced search functionality
- Export/Import: Allow users to backup and restore their todos
- Multiple Lists: Support for different todo lists or projects
- Cloud Sync: Integrate with backend services for data synchronization
13.2 UI/UX Improvements
- Dark Mode: Implement theme switching with CSS custom properties
- Animations: Add smooth transitions and micro-interactions
- Progressive Web App: Make your app installable and offline-capable
- Mobile Optimization: Enhance touch interactions and mobile layouts
14. Conclusion
Building a todo app with Svelte is an excellent way to learn the framework's modern features and best practices. Svelte's compile-time approach, reactive system, and TypeScript integration make it an ideal choice for building interactive applications.
By following this comprehensive guide, you've created a robust, feature-rich todo application that demonstrates Svelte's capabilities while providing a solid foundation for more complex applications. The skills you've learned—component architecture, state management, TypeScript integration, and modern Svelte patterns—will serve you well in building larger, more sophisticated applications.
Remember to focus on user experience, performance, and maintainability as you continue to enhance your Svelte todo app. The framework's simplicity and power make it an excellent choice for both beginners and experienced developers looking to build modern web applications.
The complete source code and build guide are available in the repository here, providing you with a reference implementation and step-by-step instructions for building your own Svelte todo application.