Skip to content

List Rendering

Rendering a list in Lit is just mapping an array to an array of templates. This lesson covers the basic pattern, its limitations, and when you need something more robust.

Rendering a List with Array.map()

The simplest way to render a list in Lit is calling .map() on your data array inside a template expression, returning one html template per item. Lit renders the resulting array of TemplateResults as a sequence of DOM nodes.

This approach works well for lists that are only ever appended to or fully replaced. It becomes less efficient for lists that are reordered, filtered, or have items removed from the middle, because Lit's default diffing compares items by their position in the array, not by identity.

const users = [
  { id: 1, name: 'Ada' },
  { id: 2, name: 'Grace' },
];

html`
  <ul>
    ${users.map((user) => html`<li>${user.name}</li>`)}
  </ul>
`;

Each render re-runs .map() and produces a fresh array of templates; Lit compares them position-by-position against the previous render.

map() vs repeat() at a Glance

${items.map((item) => html`<li>${item.name}</li>`)}
// -> compares by array index/position

${repeat(items, (item) => item.id, (item) => html`<li>${item.name}</li>`)}
// -> compares by stable key, correctly reuses/moves DOM nodes
  • .map() is the right default for static or append-only lists — it's simpler and has less overhead.
  • repeat() (covered in its own lesson) tracks each item by a stable key, so reordering moves existing DOM nodes instead of recreating them.
  • Without repeat(), reordering a list can cause focus, scroll position, or CSS transition state to jump to the wrong item.
  • Always give list items a real key candidate (a database id, not the array index) if you plan to use repeat().

List Rendering Cheat Sheet

Choosing between the common list-rendering approaches.

Scenario Approach
Static list, never reorders .map()
List with add/remove at the end only .map()
List that reorders, filters, or has stateful items (inputs, animations) repeat() with a stable key
Very large list, only a visible window rendered Virtualization (e.g. @lit-labs/virtualizer)
Empty list Render a fallback message, e.g. `items.length ? ... : html`<p>No items</p>`

Handling Empty and Loading States

A list template should almost always account for the empty case explicitly, rather than silently rendering an empty <ul>. Combine the list-rendering expression with a conditional for a much better user experience.

render() {
  return html`
    <ul>
      ${this.items.length === 0
        ? html`<li class="empty">No items yet.</li>`
        : this.items.map((item) => html`<li>${item.name}</li>`)}
    </ul>
  `;
}

Why Keys Matter for Anything Stateful

If list items contain their own internal state — a focused <input>, a CSS transition, an expanded/collapsed toggle — position-based diffing from .map() can associate the wrong DOM node with the wrong data item after a reorder, since Lit only compares by index. repeat() solves this by keying on stable item identity instead of position.

  • Use .map() freely for read-only, non-reorderable lists like a list of tags or read-only rows.
  • Switch to repeat() as soon as a list is sortable, filterable, or contains focusable/animated elements.
  • Never use the array index as the key passed to repeat() — it defeats the purpose, since the index changes on reorder too.

Common Mistakes

  • Using .map() for a sortable or filterable list and being surprised when focus/animation state ends up on the wrong row after a change.
  • Using the array index as a stable key with repeat(), which behaves the same as no key at all.
  • Rendering an empty <ul> with no items and no empty-state message, leaving users confused about whether data is still loading.
  • Rendering thousands of items directly without virtualization, causing slow initial render and scroll performance.

Key Takeaways

  • .map() over an array is the default, simplest way to render a list of templates in Lit.
  • Position-based diffing works fine for static or append-only lists but can misattribute state on reorder.
  • Reach for the repeat() directive with a stable key whenever a list reorders or contains stateful items.
  • Always design an explicit empty-state branch for list templates.

Pro Tip

Default to .map() while a list is simple, and treat 'do items in this list ever get reordered, filtered, or contain an <input>?' as the exact trigger question for switching to repeat() before a bug shows up in production.