S

Building a Modern Todo App with Svelte: Complete Step-by-Step Guide

PinoyFreeCoder
Tue Jul 15 2025
building-modern-todo-app-svelte-complete-step-by-step-guide

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.

Start Your Online Store with Shopify

Build your e-commerce business with the world's leading platform. Get started today and join millions of successful online stores.

🎉 3 MONTHS FREE for New Users! 🎉
Get Started
shopify