Skip to content
On this page What it handles for you

Display

Live List

A list that fills while rows stream in over SSE, WebSocket or a bulk job, with progress, batched screen reader announcements and auto-scroll that never steals focus.

<script>
  import { LiveList } from "@codeatlasdev/ui";

  let items = $state([]);
  let status = $state("running");

  const source = new EventSource("/api/check");
  source.onmessage = (e) => items.push(JSON.parse(e.data));
  source.addEventListener("done", () => { status = "done"; source.close(); });
</script>

<LiveList {items} total={domains.length} {status} label="Domain checks">
  {#snippet item(row)}
    <span>{row.domain}</span>
    <span>{row.available ? "Available" : "Taken"}</span>
  {/snippet}
</LiveList>

AppList and DataTable assume data that is already there. LiveList is for rows that arrive over time: a bulk checker, an import, a log viewer. Append to items as messages arrive; the component does the rest.

What it handles for you

Concern Behaviour
Screen readers The list is a named role="log" with aria-live="off", so a screen reader is not flooded with one announcement per row. New rows are summarised as a single polite message per batch (“12 new items”), and completion is announced once. Errors are announced assertively.
Auto-scroll Follows the newest row only while the user is at the bottom. Scrolling up pauses it and shows a “N new” button; pressing it jumps to the latest row and resumes. Turn it off with follow={false}.
Focus Nothing in LiveList calls focus(). A row action or a field outside the list keeps focus while rows stream in.
Progress Determinate (“3 of 12” and a bar) when you pass total; a plain count and an indeterminate bar while status="running" otherwise.
Keyboard The scrolling area becomes focusable only while it overflows, so it can be scrolled with the arrow keys.
#130

Streaming rows

Start the check, then scroll up while it runs: following pauses and a New button appears.

0 of 40
Waiting for results…

Reference #130 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { LiveList, Button, Chip } from '@codeatlasdev/ui';
  import { onDestroy } from 'svelte';

  type Check = { domain: string; free: boolean };
  const names = ['atlas', 'nimbus', 'orbit', 'prism', 'quill', 'ripple', 'sable', 'tundra', 'umbra', 'vertex', 'willow', 'zephyr'];
  const TOTAL = 40;

  let items = $state<Check[]>([]);
  let status = $state<'idle' | 'running' | 'done'>('idle');
  let timer: ReturnType<typeof setInterval> | undefined;

  function start() {
    items = [];
    status = 'running';
    clearInterval(timer);
    timer = setInterval(() => {
      const i = items.length;
      items.push({ domain: `${names[i % names.length]}${i}.com`, free: i % 3 !== 0 });
      if (items.length >= TOTAL) {
        clearInterval(timer);
        status = 'done';
      }
    }, 150);
  }
  onDestroy(() => clearInterval(timer));
</script>

<div class="wrap">
  <Button variant="filled" onclick={start} disabled={status === 'running'}>
    {status === 'running' ? 'Checking…' : 'Start bulk check'}
  </Button>
  <LiveList {items} total={TOTAL} {status} label="Domain checks" height="14rem">
    {#snippet item(row)}
      <span class="domain">{row.domain}</span>
      <Chip size="sm" variant={row.free ? 'success' : 'default'} label={row.free ? 'Available' : 'Taken'} />
    {/snippet}
  </LiveList>
</div>

<style>
  .wrap { display: grid; gap: var(--ca-space-3); width: 100%; }
  .domain { flex: 1; font-family: var(--ca-font-mono); }
</style>

Rules for the row snippet

  • Do not put role="status" or aria-live inside a row. StatusChip has role="status" on purpose (it is a live region), so inside a LiveList use Chip or plain text, or every row would announce itself.
  • Rows are append-only. Keep a stable getKey if you ever replace the array with a reordered copy.
  • A row can hold interactive content (buttons, links). Focus is never moved, so those controls stay usable while the list grows.

Props

PropTypeDefaultDescription
itemsreadonly T[]–Rows received so far, oldest first. Append to it as messages arrive.
itemSnippet<[T, number]>–Renders one row (inside an `li`). Receives the row and its index.
labelstring–Accessible name of the log region and of the progress bar.
totalnumber–Expected number of rows. Enables determinate progress.
status'idle' | 'running' | 'done' | 'error''idle'`running` shows progress; `done` and `error` are announced once.
followbooleantrueFollow the newest row while the user is at the bottom.
heightstring'24rem'Height of the scrolling area (any CSS length).
getKey(item: T, index: number) => string | numberindexStable key per row.
announceDelaynumber600Quiet period in ms before a batch of new rows is announced.
emptySnippet–Content shown while there are no rows.
classstring''Extra class names.