---
title: Streaming a response
description: Pass accumulated Markdown, signal when input is still open, and keep one Smoothstream instance per answer.
---

Smoothstream does not subscribe to your model. Your application owns the transport. On each update, pass the **full Markdown received so far** and whether more of that same answer may still arrive.

That contract is the same in React, Vue, and the DOM adapter. Only the API shape changes: `children`, a `markdown` prop, or `update()`.

## Pass accumulated Markdown

Transport packets are an implementation detail. These two deliveries should present the same way:

```text
"Hello " + "world"
```

```text
"H" + "ello wor" + "ld"
```

Keep a string that grows as tokens arrive, and pass that string into Smoothstream. If several updates land in one frame, the adapters coalesce them and plan from the latest snapshot.

<Callout type="warning" title="One answer per instance">
  Source is append-only. Replacing the string with a different response throws. Give the next answer a new React or Vue `key`, or a new DOM controller.
</Callout>

## Signal that input is still open

`receiving` is the end-of-input flag, not a playback mode.

| `receiving` | Meaning |
| --- | --- |
| `true` | This snapshot may still grow or change shape. Unsafe Markdown stays withheld. |
| `false` | This answer is complete. Remaining safe content can flush and finish presenting. |

The default is `false`. For a live generation, set `receiving` to `true` as soon as you mount the stream, then flip it to `false` when the model finishes. Leave the instance mounted so the scheduled reveal can complete.

## Choose streaming or static

`mode` controls how content enters the page. It is independent of reduced-motion policy.

| `mode` | Use it for |
| --- | --- |
| `"streaming"` (default) | The answer that is still arriving, or that has just finished and is still presenting. |
| `"static"` | History, cached messages, and other Markdown that should appear immediately. |

Do not switch a live instance from `"streaming"` to `"static"` when `receiving` becomes `false`. Close input and let presentation finish. Mount a **new** instance with `mode="static"` when the Markdown was already complete at mount time.

<Tabs>
  <Tab label="React">
    ```tsx
    {history.map((message) => (
      <Smoothstream key={message.id} mode="static">
        {message.content}
      </Smoothstream>
    ))}
    <Smoothstream key={active.id} receiving={active.receiving}>
      {active.content}
    </Smoothstream>
    ```
  </Tab>
  <Tab label="Vue">
    ```vue
    <Smoothstream
      v-for="message in history"
      :key="message.id"
      :markdown="message.content"
      mode="static"
    />
    <Smoothstream
      :key="active.id"
      :markdown="active.content"
      :receiving="active.receiving"
    />
    ```
  </Tab>
  <Tab label="Vanilla">
    ```ts
    const history = createSmoothstream(historyEl, { mode: "static" });
    history.update(previousMessage.content);

    const live = createSmoothstream(liveEl, { receiving: true });
    live.update(partial, { receiving: true });
    live.update(complete, { receiving: false });
    ```
  </Tab>
</Tabs>

## What readers see while input is open

Smoothstream is not a generic typewriter over whatever the parser currently believes. While `receiving` is `true`:

- Inline emphasis, strikethrough, links, and inline code wait until their delimiters are confirmed.
- Links stay non-interactive until the label has finished entering.
- Fenced code commits complete lines; an unfinished last line flushes when input closes.
- Tables hold extra rows so column widths can settle before a row is revealed.
- Lists reveal completed items without waiting for the entire list to close.
- Images start loading as soon as the URL is known, without blocking later blocks.

The destination is ordinary semantic HTML. Temporary reveal spans compact away when a block has settled.

## Accessibility

The Markdown root is a normal document subtree, not a live region. Do not put `aria-live`, `role="status"`, `role="log"`, or `role="alert"` on that root or an ancestor. Character admission and compaction would then announce continuously.

Each streaming instance owns a separate, visually hidden `role="status"` region. After `receiving` is `false`, presentation has finished, and pending resources have settled, it announces `Content ready.` once. Screen-reader users then navigate the semantic headings, lists, links, and tables as usual.

`mode="static"` skips that announcement. Use it for history that is already on the page.

<AccordionGroup>
  <Accordion title="What if my transport delivers the whole document at once?">
    Pass that complete string with `receiving={false}` (or omit `receiving`). Smoothstream still presents on its schedule unless you use `mode="static"` or a reduced-motion policy that renders immediately.
  </Accordion>
  <Accordion title="What if I need to show a different answer in the same slot?">
    Tear down the current instance. In React and Vue, change `key`. In the DOM adapter, call `destroy()` and `createSmoothstream` again. Updating in place with a string that is not a prefix of the previous source is an error.
  </Accordion>
</AccordionGroup>
