---
title: Completion callbacks
description: Show follow-up UI when the final Markdown unit appears, or wait until its entrance finishes.
---

Smoothstream exposes two milestones for each mounted response. Both require `receiving` to become `false` and fire once, in order:

| Milestone | When it fires | Typical use |
| --- | --- | --- |
| Reveal complete | The final presentation unit has entered the committed DOM. Its entrance animation may still be running. | Show suggested replies or another application component. |
| Presentation complete | All Smoothstream entrances have finished and temporary reveal nodes have compacted to semantic HTML. | Measure the final layout or run work that needs settled Markdown. |

React and the DOM adapter call `onRevealComplete` and `onPresentationComplete`. Vue emits `reveal-complete` and `presentation-complete`. Closing input does not itself mean either milestone has fired. Browser paint cannot be observed precisely, so reveal complete means the final unit is committed and beginning its entrance, rather than a guarantee about an exact painted pixel.

## Put a component between Markdown segments

Keep application controls outside Smoothstream's managed Markdown subtree. Show them at reveal complete, then mount a separate Smoothstream instance for the selected continuation. Each instance parses its own Markdown document, so finish the first segment at a complete block boundary.

<Tabs>
  <Tab label="React" icon="/icons/react.svg">
    ```tsx
    import { useState } from "react";
    import { Smoothstream } from "@smoothstream/react";

    function Response({ markdown, receiving }: { markdown: string; receiving: boolean }) {
      const [showChoices, setShowChoices] = useState(false);
      const [settled, setSettled] = useState(false);
      const [next, setNext] = useState<string | null>(null);

      return (
        <>
          <Smoothstream
            receiving={receiving}
            onRevealComplete={() => setShowChoices(true)}
            onPresentationComplete={() => setSettled(true)}
          >
            {markdown}
          </Smoothstream>
          {showChoices && (
            <div role="group" aria-label="Choose what comes next">
              <button onClick={() => setNext("An example follows.")}>Example</button>
              <button onClick={() => setNext("Here are more details.")}>More detail</button>
            </div>
          )}
          {next && <Smoothstream key={next} receiving={false}>{next}</Smoothstream>}
          {settled && <p>The first segment has settled.</p>}
        </>
      );
    }
    ```

    Give `Response` a new `key` for a different answer so its local choice state resets along with the stream.
  </Tab>
  <Tab label="Vue" icon="/icons/vue.svg">
    ```vue
    <script setup lang="ts">
    import { ref } from "vue";
    import { Smoothstream } from "@smoothstream/vue";

    defineProps<{ markdown: string; receiving: boolean }>();
    const showChoices = ref(false);
    const settled = ref(false);
    const next = ref<string | null>(null);
    </script>

    <template>
      <Smoothstream
        :markdown="markdown"
        :receiving="receiving"
        @reveal-complete="showChoices = true"
        @presentation-complete="settled = true"
      />
      <div v-if="showChoices" role="group" aria-label="Choose what comes next">
        <button @click="next = 'An example follows.'">Example</button>
        <button @click="next = 'Here are more details.'">More detail</button>
      </div>
      <Smoothstream v-if="next" :key="next" :markdown="next" :receiving="false" />
      <p v-if="settled">The first segment has settled.</p>
    </template>
    ```

    Give the component a new `:key` for a different answer so its choice state starts fresh.
  </Tab>
  <Tab label="Vanilla DOM" icon="/icons/javascript-typescript.svg">
    ```ts
    import { createSmoothstream } from "@smoothstream/dom";

    const firstHost = document.querySelector<HTMLElement>("#first")!;
    const choicesHost = document.querySelector<HTMLElement>("#choices")!;
    const nextHost = document.querySelector<HTMLElement>("#next")!;
    let continuation: ReturnType<typeof createSmoothstream> | undefined;

    const first = createSmoothstream(firstHost, {
      receiving: true,
      onRevealComplete() {
        const button = document.createElement("button");
        button.textContent = "Show an example";
        button.onclick = () => {
          continuation?.destroy();
          continuation = createSmoothstream(nextHost, { receiving: false });
          continuation.update("An example follows.");
        };
        choicesHost.replaceChildren(button);
      },
      onPresentationComplete() {
        // The first segment's entrance and DOM compaction are finished.
      },
    });

    first.update("## Answer\n\nThe beginning", { receiving: true });
    first.update("## Answer\n\nThe beginning and the end.", { receiving: false });
    // On removal: first.destroy(); continuation?.destroy();
    ```

    Create the second controller in a separate host; the first controller owns its own root. The two updates shown are illustrative accumulated snapshots. Continue updating with every received snapshot while input is open.
  </Tab>
</Tabs>

## Lifecycle details

- A final image waits until it has decoded or failed before entering, so it can delay reveal complete. In animated streaming mode, resources that gate reveal have resolved or failed before presentation complete. Images can still load afterward in static or reduced-motion mode.
- Static mode and reduced motion can deliver both callbacks in the same commit. An empty response also delivers both. Neither fires while `receiving` is `true`.
- Unmounting or destroying the instance cancels pending callbacks. For React and Vue, changing timing props rebuilds playback; keep them stable within a response.
- The callbacks do not replace Smoothstream's accessibility announcement. Keep your follow-up controls accessible and outside the Markdown subtree; see [Accessibility](/guides/accessibility).

In the repository, run `npm run demo`, `npm run demo:vue`, or `npm run demo:vanilla`, then select **Callbacks: interactive handoff** to see the milestones and continuation stream.
