Keeping DOM State Stable in Elm Lists with Html.Keyed
When Elm reorders or deletes a list row, the runtime diffs positionally, shifting focus and typed input to the wrong row. Using Html.Keyed with stable data-derived keys preserves DOM node identity, keeping input state anchored to the logical item.
24 Jun 2026, 12:59 UTC

The positional diffing problem
\nWhen you render a list of rows in Elm, each element becomes a DOM node. The runtime diffs the new list against the old one by index. If you reorder or delete the first item, the runtime may reuse the DOM node that was previously at index 1, shifting its position. Any typed text or focus sitting in that node ends up under the wrong row. This is the default behavior when you use Html.ul or Html.ol with plain Html children.
Introducing Html.Keyed
\nElm's Html.Keyed module in the elm/html package exposes node, ol, and ul, which take a list of (String, Html msg) pairs instead of plain Html children. The string is an identity key: during diffing, Elm matches old and new children by key rather than by index, so the same logical item keeps the same DOM node across renders.
Key-value pairs
\nRender rows with Html.Keyed.ul [] (List.map (\\t -> (t.id, viewRow t)) tasks) using a stable id from the model. Deleting the first task then removes exactly that row node instead of shifting every input value down one position the classic unkeyed bug.
When plain Html.ul still fits
\nKeys add bookkeeping (the model must carry a stable identifier) plus a small overhead for key bookkeeping in the diff, and they only affect node identity attribute and child diffing still runs. For static or append-only lists, plain Html functions stay simpler and are perfectly fine.
\n\nVerifying the behavior
\nTo see the difference yourself, build a minimal Elm app with rows containing text inputs. Render one version with Html.ul and another with Html.Keyed.ul, each using the same task data. Delete or reorder the first row in both and watch where typed text and focus land. The keyed version keeps input state anchored to the logical item the unkeyed version shifts it.
Elm is statically typed and purely functional with no null Maybe Result instead and famously descriptive compiler errors so the state moved rows bug is deterministic and easy to demo in a tiny repo which suits an approachable blog.
\n\nThe long-standing stable compiler release is Elm 0.19.1 and elm/html 1.0.x provides Html.Keyed; the modules official documentation includes explicit guidance on when keyed nodes are worth using.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.