Skip to content
Personal

TaskFlow

A to-do app built from first principles — plain HTML, CSS, and vanilla JavaScript, one milestone at a time, with a genuine EN/FR toggle.

Role
Design & development, solo
Year
2026
Stack
HTML5, CSS3, vanilla JS
Status
Complete

TaskFlow is a to-do list app built one milestone at a time to practice DOM manipulation, state management, and persistence from first principles — no frameworks, no build tools, no dependencies. It does the ordinary things a task app should: add, edit, complete, delete, filter, search — and does each of them without cutting a state-management corner.

What makes it work

  • One array as the only source of truth. The tasks array in app.js is the sole place task data lives during a session. The DOM is always fully rebuilt from it via renderTasks() — never patched or edited directly — so the UI can never drift from the data.
  • A real EN/FR toggle, not a placeholder. A data-i18n dictionary swaps every string client-side — labels, empty states, ARIA text — the same pattern used on Bénin Horizons and Luna Bistro, verified live on the deployed demo.

Design at a glance

  • bg · #f4f5f7
  • surface · #ffffff
  • accent · #2f6f4f
  • text · #1f2933

System font stack throughout — zero custom fonts, zero font-loading delay.

Built to last

  • Tasks persist across reloads via localStorage
  • Two distinct empty states — "no tasks yet" vs. "no results for your search/filter" — never a bare blank list
  • Responsive across mobile, tablet, and desktop, with visible focus states on every control

Want a tool this considered built for your own workflow?

See it live Start a project
For developers — the full technical breakdown

01. State-driven rendering over direct DOM edits

Problem
A to-do list's visible state can silently drift from its real data if list items are edited directly in the DOM instead of through one source of truth.
Choice
A single tasks array drives every render; the DOM is fully rebuilt from it on every change via renderTasks(), never patched in place.
Result
Filtering, searching, and the live stats counts are always consistent with the underlying data — there's no code path where the UI can show something the array doesn't.

02. Event delegation instead of per-task listeners

Problem
Tasks are created and destroyed constantly; attaching a fresh listener to every new task row doesn't scale cleanly and risks orphaned handlers.
Choice
One delegated listener on the list container handles edit/complete/delete clicks for any row, existing or newly rendered.
Result
New tasks are interactive immediately with no re-binding step, and the full-rebuild-per-render approach never leaks a stale listener.

Accessibility & performance

  • Visible :focus-visible states on the language toggle and :focus states on the form input and search field
  • Distinct, explicit empty states for "no tasks yet" vs. "no search/filter results"
  • System font stack throughout — zero custom fonts, zero font-loading delay
  • Responsive layout verified at mobile, tablet, and desktop widths

What I’d do differently

No automated test suite — each milestone was verified manually. Reasonable at this size, but a v2 would benefit from a small unit-test pass specifically on the filter/search derivation logic, since that's the part most likely to regress silently as features are added.

Stack

  • HTML5
  • CSS3
  • Vanilla JavaScript (ES6+)
  • localStorage