When to Use Html.Keyed vs Plain Html for Lists in Elm 0.19
Guide for deciding between Html.Keyed and plain Html lists in Elm 0.19, with constraints, trade-offs, example code, and validation steps.
29 Dec 2025, 11:34 UTC

Decision and constraints
You need to render a list of items where the order can change (insert, delete, reorder) and you want to keep per-item DOM state such as focus, cursor position in an input, or scroll offset. The decision must be made within the Elm 0.19 Html API, which offers two ways to build a list:
- Plain
Html.ulwithList.map– items are matched by position. - Keyed
Html.Keyed.node(orHtml.Keyed.ul) – items are matched by a stable string key you provide.
The constraint is that each item must expose an identifier that does not change when the list is reordered; this identifier will become the key.
Options comparison
| Option | How it works | When it is appropriate |
|---|---|---|
Plain Html.ul | Renders List.map item view; the virtual DOM diff assumes the nth node corresponds to the nth item. | Static lists, append-only logs, or any list where order never changes after the initial render. |
Keyed Html.Keyed.node | Renders a list of (key, node) tuples; the diff reuses existing DOM nodes when the same key appears elsewhere in the new list. | Lists where items can be inserted, removed, or moved and you need to preserve local DOM state (focus, input value, internal component state). |
Trade-offs
Keyed advantages
- DOM nodes stay attached to the same logical item, so focus, selection, and uncontrolled input values survive reordering.
- Moving an item causes only a node move in the real DOM, reducing layout thrash and repaint cost.
Keyed disadvantages
- You must generate and maintain a unique, stable string key for each item.
- If a key changes unintentionally (e.g., using the list index), the diff treats it as a new item and remounts, losing state.
- Slight runtime overhead for the key map and extra boilerplate in the view.
Plain advantages
- Simpler code, no key management.
- Zero overhead for static or append-only scenarios.
Plain disadvantages
- Reordering forces the virtual DOM to treat every position as changed, causing all nodes to be destroyed and recreated.
- Any per-node DOM state (focus, input text, scroll) is lost on each reorder.
Concrete implementation
Assume a model that holds a list of tasks, each with an immutable id field.
type alias Model =
{ tasks : List Task }
type alias Task =
{ id : Int
, description : String
}
In the update function you can reorder the list, for example by moving a task up:
type Msg = MoveUp Int
update : Msg -> Model -> Model
update msg model =
case msg of
MoveUp idx -
let
tasks = model.tasks
-- simple swap with previous element if not already at top
newTasks =
if idx <= 0 || idx >= List.length tasks then
tasks
else
let
(before, target::_after) = List.splitAt idx tasks
targetElem = List.head target |> Maybe.withDefault (Debug.crash "unexpected")
after = List.tail target |> Maybe.withDefault []
in
before ++ [ targetElem ] ++ List.take (idx-1) tasks ++ after
in
{ model | tasks = newTasks }
_ -> model
The view uses Html.Keyed.node for the list:
view : Model -> Html Msg
view model =
let
taskView task =
( String.fromInt task.id -- stable key
, Html.li []
[ Html.input [ Html.placeholder task.description, Html.value task.description ] []
, Html.button [ onClick (MoveUp task.id) ] [ Html.text "↑" ]
]
)
keyedTasks = List.map taskView model.tasks
in
Html.div []
[ Html.h2 [] [ Html.text "Task list" ]
, Html.Keyed.node "ul" [] keyedTasks
]
If you replace the keyed node with a plain list, the same view becomes:
viewPlain : Model -> Html Msg
viewPlain model =
Html.div []
[ Html.h2 [] [ Html.text "Task list" ]
, Html.ul [] (List.map (\t -> Html.li [] [ Html.input [ Html.placeholder t.description, Html.value t.description ] [] , Html.button [ onClick (MoveUp t.id) ] [ Html.text "↑" ] ]) model.tasks)
]
Validation approach
- Compile the example with
elm make src/Main.elm --output=main.jsusing Elm 0.19. - Run the generated page in a browser.
- Focus on an input inside a list item, then click the "↑" button to move that item up.
- Observe:
- With the keyed view, the input remains focused and its value stays attached to the same logical task.
- With the plain view, the input loses focus and the value appears to move with the position, demonstrating state loss.
- Optionally, add a simple
elm-testsnapshot that asserts the list of keys rendered byHtml.Keyed.nodematches the set ofTask.idvalues after aMoveUpmessage.
Limitations and cautions
- Keys must be unique across the current render; duplicate keys cause undefined diff behavior.
- Never use the list index as a key for data that can reorder; the index changes on every move and defeats the purpose of keying.
- If an item's identifier can change (e.g., a temporary UI-only flag), derive a separate stable id for keying.
- For very small static lists (under ~5 items) the overhead of keyed nodes may not be worth the benefit; plain nodes are simpler and performant enough.
By following the decision guide above you can choose the appropriate rendering strategy for your Elm 0.19 front-end lists while keeping DOM state stable when it matters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.