Skip to content

Todo List

A complete todo list application with add, toggle, and remove functionality.

Code

tsx
import { signal, computed, For, Show, render } from 'kaori.js';

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

function TodoApp() {
  const todos = signal<Todo[]>([]);
  const input = signal('');

  const remaining = computed(
    () => todos.value.filter(t => !t.completed).length
  );

  function addTodo() {
    const text = input.value.trim();
    if (!text) return;

    todos.value = [...todos.value, { id: Date.now(), text, completed: false }];
    input.value = '';
  }

  function toggleTodo(id: number) {
    todos.value = todos.value.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
  }

  function removeTodo(id: number) {
    todos.value = todos.value.filter(todo => todo.id !== id);
  }

  function handleKeyDown(e: KeyboardEvent) {
    if (e.key === 'Enter') {
      addTodo();
    }
  }

  return () => (
    <div class="todo-app">
      <h1>My Todos ✨</h1>

      <div class="input-section">
        <input
          type="text"
          prop:value={input.value}
          onChange={e => (input.value = e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder="What needs to be done?"
        />
        <button onClick={addTodo}>Add</button>
      </div>

      <Show
        when={todos.value.length > 0}
        fallback={() => <p class="empty">No todos yet!</p>}
      >
        <ul class="todo-list">
          <For items={todos.value} key={todo => todo.id}>
            {todo => (
              <li classMap={{ completed: todo.completed }}>
                <input
                  type="checkbox"
                  bool:checked={todo.completed}
                  onChange={() => toggleTodo(todo.id)}
                />
                <span class="todo-text">{todo.text}</span>
                <button class="delete-btn" onClick={() => removeTodo(todo.id)}>
                  ×
                </button>
              </li>
            )}
          </For>
        </ul>

        <div class="footer">
          <span>{remaining.value} items left</span>
        </div>
      </Show>
    </div>
  );
}

render(<TodoApp />, document.getElementById('root')!);

Styling

css
.todo-app {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(255, 107, 157, 0.1);
}

.todo-app h1 {
  text-align: center;
  color: #ff6b9d;
  margin-bottom: 2rem;
}

.input-section {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.input-section input {
  flex: 1;
  padding: 0.75rem;
  border: 2px solid #ffd6e5;
  border-radius: 8px;
  font-size: 1rem;
}

.input-section button {
  padding: 0.75rem 2rem;
  background: #ff6b9d;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.2s;
}

.input-section button:hover {
  background: #ff4d88;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem;
  border-bottom: 1px solid #ffd6e5;
  transition: background 0.2s;
}

.todo-list li:hover {
  background: #fef3f7;
}

.todo-list li.completed .todo-text {
  text-decoration: line-through;
  opacity: 0.5;
}

.todo-text {
  flex: 1;
  font-size: 1rem;
}

.delete-btn {
  background: none;
  border: none;
  color: #ff6b9d;
  font-size: 1.5rem;
  cursor: pointer;
  padding: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: background 0.2s;
}

.delete-btn:hover {
  background: #ffe0eb;
}

.footer {
  margin-top: 1rem;
  padding: 1rem;
  text-align: center;
  color: #90a4b7;
}

.empty {
  text-align: center;
  color: #90a4b7;
  padding: 2rem;
}

Made with 💖 by golok