Building a progressively enhanced todo with modern web standards

Recently I need to use a mutation observer and dutifully searched for “mdn mutation observer”. It led to me this blog post, and it had a demo of a regular list with contenteditable. What I neither knew nor expected was that contenteditable on a list means you can add new items by pressing enter. It started me wondering if I could build a decent todo list—like framework devs used to do and boast about it being only 80 megabytes or whatever. So, for the craic, I had a go and this is how I did it.

If you’re like me you’ll want to see the demo first and code.

1. The list

So the basic functionality is a list (ordered or unordered) with the contenteditable attribute like so:

<ul contenteditable>

And that’s it! I’ve been writing HTML for over 20 years and it still surprises me.

I couldn’t face downloading a Windows XP virtual machine to verify, but contenteditable is supported in some form by IE6, so this basic functionality might just work in old Internet Explorer 6. Anyway, a quick demo:

An HTML only list that is fully editable

2. Checkboxes

On to the first enhancement, being able to check items off the list. As this is progressive enhancement I could dive right into building some ES modules for the functionality and not worry about the tangle of conditions required to make it work everywhere. The first module I built takes a list item and wraps the text inside a <label> element with a checkbox beside it.

One little wrinkle here is that checkboxes inside a parent with contenteditable can’t be checked in Firefox. I had to wrap each one in a span with the contenteditable attribute set to false like so:

<li><label><span contenteditable="false"><input type="checkbox"></span>Item</label></li>

As luck would have it I’d just been looking up mutation observers, so used one to watch for new elements. Remember every time you press enter in a list with contenteditable it creates a new <li> element. So whenever the mutation obvserver observed the new <li> it runs the module to add a checkbox to list items that don’t already have one.

With about 30 lines of JS and a little bit of CSS it now works like this:

An editable list with checkboxes

Incidentally, I used the new :has() pseudo-class for the line through the item when checked, and at time of writing it only works in Safari and Chrome Canary.

li:has(input:checked) {
  text-decoration: line-through;

Again, this is progressive enhancement so I didn’t need to write JS to toggle a class. It’s clear from the checkboxes themselves which ones are checked and this is a nice little addition for supporting browsers.


So far it’s working as an editable checklist until the page is refreshed, then it all disappears.