frontend-101
GitHub
The DOM
Responsiveness
Components
State
API Calls
Frameworks
OverviewSee the DifferenceLandscape
Accessibility
API CallsAccessibility

frontend-101 — a learning resource for new frontend devs.

© 2026 · MIT LicenseFound an issue?

See the Difference

The same task, two approaches.

Interactive demo

Below is the same todo list built twice: once in vanilla JavaScript, once in React. The live list is at the top; under it, the two implementations sit side by side (tabbed on narrow screens). Before you read them: which version do you think is shorter, and by how much?

What To Do?

No todos yet. Type something and press Enter or click Add.

Vanilla JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<script>
const input = document.getElementById("todo-input");
const addBtn = document.getElementById("add-btn");
const list = document.getElementById("todo-list");
const counter = document.getElementById("counter");

let nextId = 0;

function updateCounter() {
  const total = list.children.length;
  const done = list.querySelectorAll(".completed").length;
  counter.textContent = done + "/" + total + " completed";
}

addBtn.addEventListener("click", () => {
  const text = input.value.trim();
  if (!text) return;

  const id = nextId++;
  const li = document.createElement("li");
  li.className = "todo-item";
  li.dataset.id = id;

  // Toggle button
  const toggleBtn = document.createElement("button");
  toggleBtn.textContent = "○";
  toggleBtn.addEventListener("click", () => {
    li.classList.toggle("completed");
    const span = li.querySelector("span");
    if (li.classList.contains("completed")) {
      toggleBtn.textContent = "✓";
      span.style.textDecoration = "line-through";
      span.style.opacity = "0.5";
    } else {
      toggleBtn.textContent = "○";
      span.style.textDecoration = "";
      span.style.opacity = "";
    }
    updateCounter();
  });

  // Text
  const span = document.createElement("span");
  span.textContent = text;

  // Delete button
  const deleteBtn = document.createElement("button");
  deleteBtn.textContent = "✕";
  deleteBtn.addEventListener("click", () => {
    list.removeChild(li);
    updateCounter();
  });

  li.appendChild(toggleBtn);
  li.appendChild(span);
  li.appendChild(deleteBtn);
  list.appendChild(li);

  input.value = "";
  updateCounter();
});
</script>

<!-- HTML markup -->
<div id="todo-app">
  <div>
    <input id="todo-input" type="text" placeholder="Add a todo..." />
    <button id="add-btn">Add</button>
  </div>
  <p id="counter"></p>
  <ul id="todo-list"></ul>
</div>
React
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState("");

  function addTodo() {
    if (!text.trim()) return;
    setTodos([...todos, { id: Date.now(), text, done: false }]);
    setText("");
  }

  function toggle(id) {
    setTodos(todos.map((t) =>
      t.id === id ? { ...t, done: !t.done } : t
    ));
  }

  function remove(id) {
    setTodos(todos.filter((t) => t.id !== id));
  }

  const done = todos.filter((t) => t.done).length;

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <p>{done}/{todos.length} completed</p>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <button onClick={() => toggle(t.id)}>
              {t.done ? "✓" : "○"}
            </button>
            <span style={{ opacity: t.done ? 0.5 : 1 }}>
              {t.text}
            </span>
            <button onClick={() => remove(t.id)}>✕</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<script>
const input = document.getElementById("todo-input");
const addBtn = document.getElementById("add-btn");
const list = document.getElementById("todo-list");
const counter = document.getElementById("counter");

let nextId = 0;

function updateCounter() {
  const total = list.children.length;
  const done = list.querySelectorAll(".completed").length;
  counter.textContent = done + "/" + total + " completed";
}

addBtn.addEventListener("click", () => {
  const text = input.value.trim();
  if (!text) return;

  const id = nextId++;
  const li = document.createElement("li");
  li.className = "todo-item";
  li.dataset.id = id;

  // Toggle button
  const toggleBtn = document.createElement("button");
  toggleBtn.textContent = "○";
  toggleBtn.addEventListener("click", () => {
    li.classList.toggle("completed");
    const span = li.querySelector("span");
    if (li.classList.contains("completed")) {
      toggleBtn.textContent = "✓";
      span.style.textDecoration = "line-through";
      span.style.opacity = "0.5";
    } else {
      toggleBtn.textContent = "○";
      span.style.textDecoration = "";
      span.style.opacity = "";
    }
    updateCounter();
  });

  // Text
  const span = document.createElement("span");
  span.textContent = text;

  // Delete button
  const deleteBtn = document.createElement("button");
  deleteBtn.textContent = "✕";
  deleteBtn.addEventListener("click", () => {
    list.removeChild(li);
    updateCounter();
  });

  li.appendChild(toggleBtn);
  li.appendChild(span);
  li.appendChild(deleteBtn);
  list.appendChild(li);

  input.value = "";
  updateCounter();
});
</script>

<!-- HTML markup -->
<div id="todo-app">
  <div>
    <input id="todo-input" type="text" placeholder="Add a todo..." />
    <button id="add-btn">Add</button>
  </div>
  <p id="counter"></p>
  <ul id="todo-list"></ul>
</div>

Both do the same thing. The framework version is shorter, declarative, and automatically keeps the UI in sync with the data.

What to notice

The live demo is the same in both worlds: a todo list you can add to, complete, and delete. The code underneath is where they part ways.

  • Vanilla JS, about 70 lines: you create each DOM element, wire an event listener to every button, toggle classes and inline styles, remove nodes, and recount the total yourself. Roughly 50 of those lines are the single “add a todo” handler.
  • React, about 40 lines: you update an array in state and return JSX. Add, complete, and delete are three short functions, a dozen lines between them.

Same behavior, less than half the hand-written wiring, and that gap only widens as the UI grows. The framework lets you spend your attention on what to show instead of how to update the page.

Next, explore the framework landscape