Skip to content

Solution: Todo List + Dark Mode Toggle

ลองเล่น Todo List ที่สมบูรณ์แล้ว — ลองเพิ่ม, ลบ, mark complete, และสลับ Dark Mode ดู

todo-app
Complete Demo INTERACTIVE

Todo List

    • Directorytodo-app/
      • index.html โครงสร้าง HTML + link CSS/JS
      • style.css ตกแต่ง style + dark mode theme
      • script.js State, Render, Events, localStorage
    Show index.html
    index.html
    <!DOCTYPE html>
    <html lang="th">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Todo App</title>
    <link rel="stylesheet" href="style.css" />
    </head>
    <body>
    <div class="app">
    <header class="header">
    <h1>Todo List</h1>
    <button id="theme-toggle" class="theme-btn" aria-label="Toggle dark mode">
    <span class="icon-sun">&#9728;</span>
    <span class="icon-moon">&#9790;</span>
    </button>
    </header>
    <form id="todo-form" class="todo-form">
    <input
    type="text"
    id="todo-input"
    class="todo-input"
    placeholder="What needs to be done?"
    autocomplete="off"
    />
    <button type="submit" class="add-btn">Add</button>
    </form>
    <ul id="todo-list" class="todo-list"></ul>
    <footer class="footer">
    <span id="todo-count">0 items left</span>
    </footer>
    </div>
    <script src="script.js"></script>
    </body>
    </html>
    Show style.css
    style.css
    /* === Reset & Base === */
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    /* === CSS Variables (Light Theme) === */
    body {
    --bg: #f5f5f5;
    --text: #1a1a2e;
    --card-bg: #ffffff;
    --input-bg: #ffffff;
    --input-border: #d4d4d8;
    --input-focus: #8b5cf6;
    --btn-bg: #8b5cf6;
    --btn-hover: #7c3aed;
    --btn-text: #ffffff;
    --item-border: #e4e4e7;
    --item-hover: #f9fafb;
    --completed-text: #a1a1aa;
    --delete-color: #ef4444;
    --footer-text: #71717a;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    font-family: "Inter", "Noto Sans Thai", system-ui, -apple-system, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 2rem 1rem;
    transition: background 0.3s ease, color 0.3s ease;
    }
    /* === Dark Theme === */
    body.dark {
    --bg: #0f0f1a;
    --text: #e4e4e7;
    --card-bg: #1a1a2e;
    --input-bg: #222240;
    --input-border: #3f3f5c;
    --input-focus: #a78bfa;
    --btn-bg: #8b5cf6;
    --btn-hover: #a78bfa;
    --btn-text: #ffffff;
    --item-border: #2e2e4a;
    --item-hover: #222240;
    --completed-text: #52525b;
    --delete-color: #f87171;
    --footer-text: #71717a;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    }
    /* === App Container === */
    .app {
    width: 100%;
    max-width: 520px;
    background: var(--card-bg);
    border-radius: 12px;
    box-shadow: var(--shadow);
    overflow: hidden;
    transition: background 0.3s ease, box-shadow 0.3s ease;
    }
    /* === Header === */
    .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.5rem 1.5rem 1rem;
    }
    .header h1 {
    font-size: 1.5rem;
    font-weight: 700;
    }
    .theme-btn {
    background: none;
    border: 2px solid var(--input-border);
    border-radius: 8px;
    padding: 0.4rem 0.6rem;
    cursor: pointer;
    font-size: 1.1rem;
    color: var(--text);
    transition: border-color 0.2s, background 0.2s;
    line-height: 1;
    }
    .theme-btn:hover {
    border-color: var(--input-focus);
    background: var(--item-hover);
    }
    /* Sun/Moon icon toggling */
    body .icon-moon {
    display: none;
    }
    body .icon-sun {
    display: inline;
    }
    body.dark .icon-moon {
    display: inline;
    }
    body.dark .icon-sun {
    display: none;
    }
    /* === Form === */
    .todo-form {
    display: flex;
    gap: 0.5rem;
    padding: 0 1.5rem 1rem;
    }
    .todo-input {
    flex: 1;
    padding: 0.65rem 0.9rem;
    border: 2px solid var(--input-border);
    border-radius: 8px;
    background: var(--input-bg);
    color: var(--text);
    font-size: 0.95rem;
    font-family: inherit;
    outline: none;
    transition: border-color 0.2s, background 0.2s;
    }
    .todo-input::placeholder {
    color: var(--footer-text);
    }
    .todo-input:focus {
    border-color: var(--input-focus);
    }
    .add-btn {
    padding: 0.65rem 1.2rem;
    background: var(--btn-bg);
    color: var(--btn-text);
    border: none;
    border-radius: 8px;
    font-size: 0.95rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: background 0.2s;
    }
    .add-btn:hover {
    background: var(--btn-hover);
    }
    /* === Todo List === */
    .todo-list {
    list-style: none;
    max-height: 400px;
    overflow-y: auto;
    }
    .todo-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1.5rem;
    border-top: 1px solid var(--item-border);
    transition: background 0.15s;
    }
    .todo-item:hover {
    background: var(--item-hover);
    }
    .todo-item input[type="checkbox"] {
    width: 1.15rem;
    height: 1.15rem;
    accent-color: var(--btn-bg);
    cursor: pointer;
    flex-shrink: 0;
    }
    .todo-item .todo-text {
    flex: 1;
    font-size: 0.95rem;
    transition: color 0.2s, text-decoration 0.2s;
    word-break: break-word;
    }
    .todo-item.completed .todo-text {
    text-decoration: line-through;
    color: var(--completed-text);
    }
    .delete-btn {
    background: none;
    border: none;
    color: var(--footer-text);
    cursor: pointer;
    font-size: 1.2rem;
    padding: 0.15rem 0.4rem;
    border-radius: 4px;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.15s, color 0.15s, background 0.15s;
    }
    .todo-item:hover .delete-btn {
    opacity: 1;
    }
    .delete-btn:hover {
    color: var(--delete-color);
    background: rgba(239, 68, 68, 0.1);
    }
    /* === Footer === */
    .footer {
    padding: 0.9rem 1.5rem;
    border-top: 1px solid var(--item-border);
    text-align: center;
    font-size: 0.85rem;
    color: var(--footer-text);
    }
    Show script.js
    script.js
    // =============================================
    // Todo List + Dark Mode Toggle
    // Vanilla JS — State → Render → Events pattern
    // =============================================
    // === Helper: Safe localStorage ===
    const loadFromStorage = (key, fallback) => {
    try {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : fallback;
    } catch (error) {
    console.error(`Failed to load "${key}" from localStorage:`, error.message);
    return fallback;
    }
    };
    const saveToStorage = (key, value) => {
    try {
    localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
    console.error(`Failed to save "${key}" to localStorage:`, error.message);
    }
    };
    // === State ===
    let todos = loadFromStorage("todos", []);
    let isDark = localStorage.getItem("theme") === "dark";
    // === DOM Elements ===
    const todoForm = document.querySelector("#todo-form");
    const todoInput = document.querySelector("#todo-input");
    const todoList = document.querySelector("#todo-list");
    const todoCount = document.querySelector("#todo-count");
    const themeToggle = document.querySelector("#theme-toggle");
    // === Render ===
    const render = () => {
    // Clear list
    todoList.innerHTML = "";
    // Build todo items
    todos.forEach((todo) => {
    const li = document.createElement("li");
    li.className = `todo-item${todo.completed ? " completed" : ""}`;
    // Checkbox
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = todo.completed;
    checkbox.addEventListener("change", () => {
    todo.completed = !todo.completed;
    save();
    render();
    });
    // Text
    const span = document.createElement("span");
    span.className = "todo-text";
    span.textContent = todo.text;
    // Delete button
    const deleteBtn = document.createElement("button");
    deleteBtn.className = "delete-btn";
    deleteBtn.textContent = "\u00d7";
    deleteBtn.setAttribute("aria-label", `Delete "${todo.text}"`);
    deleteBtn.addEventListener("click", () => {
    todos = todos.filter((t) => t.id !== todo.id);
    save();
    render();
    });
    li.appendChild(checkbox);
    li.appendChild(span);
    li.appendChild(deleteBtn);
    todoList.appendChild(li);
    });
    // Update count
    const remaining = todos.filter((t) => !t.completed).length;
    todoCount.textContent = `${remaining} item${remaining !== 1 ? "s" : ""} left`;
    };
    // === Save State ===
    const save = () => {
    saveToStorage("todos", todos);
    };
    // === Theme ===
    const applyTheme = () => {
    document.body.classList.toggle("dark", isDark);
    localStorage.setItem("theme", isDark ? "dark" : "light");
    };
    // === Events ===
    // Add todo
    todoForm.addEventListener("submit", (e) => {
    e.preventDefault();
    const text = todoInput.value.trim();
    if (!text) return;
    todos.push({
    id: Date.now(),
    text,
    completed: false,
    });
    todoInput.value = "";
    todoInput.focus();
    save();
    render();
    });
    // Toggle theme
    themeToggle.addEventListener("click", () => {
    isDark = !isDark;
    applyTheme();
    });
    // === Initialize ===
    applyTheme();
    render();
    1. HTML Structure

      โครงสร้างเรียบง่าย: .app container ครอบทั้งหมด, header มีชื่อ app + ปุ่ม theme toggle, form สำหรับเพิ่ม todo, ul#todo-list แสดงรายการ, และ footer แสดงจำนวน ไฟล์ CSS/JS ถูก link แยกเป็นไฟล์ต่างหาก

      Preview Output
      Todo List ☀/☽
      What needs to be done?
      Add
      Learn HTML ×
      Setup project ×
      Add dark mode ×
      <header> <form> <ul> <li> <footer>
    2. CSS Variables + Dark Mode

      ใช้ CSS Custom Properties (--bg, --text, --card-bg ฯลฯ) ประกาศค่าสีทั้งหมดบน body เมื่อ toggle dark mode จะเพิ่ม class dark บน body แล้ว override ค่า variables ทั้งหมด ทำให้เปลี่ยน theme ได้ง่ายและ smooth ด้วย transition

      Preview Output
      Light Mode
      Hello World
      --bg: #f5f5f5 --text: #1a1a2e --btn-bg: #8b5cf6
      body.dark
      Dark Mode
      Hello World
      --bg: #0f0f1a --text: #e4e4e7 --btn-bg: #8b5cf6
    3. State Pattern

      JavaScript จัดโครงสร้างตาม State → Render → Events:

      • State: todos array + isDark boolean
      • Render: ฟังก์ชัน render() อ่าน state แล้วสร้าง DOM ใหม่ทั้งหมด
      • Events: form submit เพิ่ม todo, checkbox toggle completed, button ลบ todo, theme toggle
      Preview Output
      State
      todos: [...] isDark: bool
      render()
      Render
      DOM Update
      addEventListener
      Event
      click / submit
      setState
      ↻ Loop
    4. createElement แทน innerHTML

      ใช้ document.createElement() + textContent สร้าง elements แทน innerHTML เพื่อป้องกัน XSS (Cross-Site Scripting) — user input จะไม่ถูก parse เป็น HTML

      Preview Output
      ×
      innerHTML = userInput XSS Risk!
      vs
      createElement() + textContent Safe
    5. localStorage + Error Handling

      ฟังก์ชัน loadFromStorage() และ saveToStorage() ครอบด้วย try/catch เพื่อป้องกัน JSON.parse error ถ้าข้อมูลเสียหาย Todo IDs ใช้ Date.now() เพื่อให้ unique ข้าม sessions

      Preview Output
      localStorage
      KeyValue
      "todos" [{id:1, text:'Learn JS'...}]
      "theme" "dark"
      saveToStorage()
      loadFromStorage()
    6. Theme Toggle

      applyTheme() ใช้ classList.toggle("dark", isDark) — parameter ที่สองบอกว่าจะเพิ่มหรือลบ class แล้วบันทึก theme ลง localStorage ทุกครั้ง

      Preview Output
      <body>
      Light
      classList.toggle("dark")
      <body class="dark">
      Dark
    7. Accessible UI

      Delete button มี aria-label สำหรับ screen reader, checkbox ใช้ native <input type="checkbox">, และ form ใช้ <form> + submit event ที่รองรับทั้งการกดปุ่มและ Enter

      Preview Output
      🗣
      aria-label
      Screen reader support
      Native <input>
      Built-in checkbox a11y
      Form submit
      Enter key support
    • ใช้ JavaScript พื้นฐาน: variables (const/let), functions (arrow), arrays (.push(), .filter(), .forEach())
    • จัดการ DOM: querySelector, createElement, textContent, classList, addEventListener
    • เข้าใจ State pattern: State → Render → Event → State loop
    • ทำ interactive features: เพิ่ม/ลบ/toggle todo, dark mode toggle
    • ใช้ localStorage: บันทึกและโหลด data + try/catch error handling
    • เขียน CSS Variables: สร้าง themeable UI ที่เปลี่ยน dark/light mode ได้ง่าย