---
title: Vanilla DOM
description: Mount Smoothstream in a browser page with createSmoothstream, then update and destroy the controller.
---

`@smoothstream/dom` is the framework-free adapter. It creates one managed root inside a container and reconciles the same keyed HTML model the React and Vue adapters use.

<CodeGroup>
```bash title="npm" hideLanguageIcon
npm install @smoothstream/dom
```

```bash title="pnpm" hideLanguageIcon
pnpm add @smoothstream/dom
```

```bash title="yarn" hideLanguageIcon
yarn add @smoothstream/dom
```

```bash title="bun" hideLanguageIcon
bun add @smoothstream/dom
```
</CodeGroup>

There is no framework peer dependency. Reveal CSS and the default prose theme import automatically.

## Create, update, destroy

```ts title="assistant-message.ts"
import { createSmoothstream } from "@smoothstream/dom";

const stream = createSmoothstream(document.querySelector("#response")!, {
  className: "assistant-markdown",
  receiving: true,
});

stream.update(markdownReceivedSoFar, { receiving: true });
stream.update(completeMarkdown, { receiving: false });

stream.destroy();
```

| Method | Role |
| --- | --- |
| `createSmoothstream(container, options)` | Mounts a root inside `container` and returns a controller. |
| `update(markdown, options?)` | Queues the latest append-only snapshot. `options.receiving` is the only value you can change after create. |
| `destroy()` | Removes the root, listeners, pending work, and the completion announcer. |
| `element` | The managed `HTMLDivElement`. |

<Callout type="tip" title="Follow the streaming contract">
  The usage rules in [Streaming a response](/get-started/streaming) apply here: accumulated snapshots, `receiving` while the model is still writing, and a new controller for a different answer.
</Callout>

## Options are mostly constructor-only

`mode`, `reveal`, `interval`, `duration`, `reducedMotion`, `unstyled`, `className`, and `codeHighlighter` are read when the controller is created. Changing them later has no effect.

To present a different response, or to use different timing, call `destroy()` and create a new controller.

```ts
const history = createSmoothstream(historyEl, { mode: "static" });
history.update(previousMessage.content);

const live = createSmoothstream(liveEl, { receiving: true });
live.update(partial, { receiving: true });
// Keep this controller in streaming mode when input closes:
live.update(complete, { receiving: false });
```

`update()` throws if the new string is not a prefix of the previous source, or if you call it after `destroy()`.

Full option list: [API](/reference/api).
